diff --git a/.clang-tidy b/.clang-tidy index fc5ce854f1..79276f81c3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,9 +2,11 @@ Checks: >- *, -abseil-*, + -altera-*, -android-*, -boost-*, -bugprone-branch-clone, + -bugprone-easily-swappable-parameters, -bugprone-narrowing-conversions, -bugprone-signed-char-misuse, -bugprone-too-small-loop-variable, @@ -14,7 +16,13 @@ Checks: >- -cert-str34-c, -clang-analyzer-optin.cplusplus.UninitializedObject, -clang-analyzer-osx.*, + -clang-diagnostic-delete-abstract-non-virtual-dtor, + -clang-diagnostic-delete-non-abstract-non-virtual-dtor, -clang-diagnostic-shadow-field, + -clang-diagnostic-sign-compare, + -clang-diagnostic-unused-variable, + -clang-diagnostic-unused-const-variable, + -concurrency-*, -cppcoreguidelines-avoid-c-arrays, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-avoid-magic-numbers, @@ -22,7 +30,6 @@ Checks: >- -cppcoreguidelines-macro-usage, -cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-non-private-member-variables-in-classes, - -cppcoreguidelines-owning-memory, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, @@ -56,17 +63,21 @@ Checks: >- -misc-no-recursion, -misc-unused-parameters, -modernize-avoid-c-arrays, + -modernize-avoid-bind, + -modernize-concat-nested-namespaces, -modernize-return-braced-init-list, -modernize-use-auto, -modernize-use-default-member-init, -modernize-use-equals-default, -modernize-use-trailing-return-type, + -modernize-use-nodiscard, -mpi-*, -objc-*, -readability-braces-around-statements, -readability-const-return-type, -readability-convert-member-functions-to-static, -readability-else-after-return, + -readability-function-cognitive-complexity, -readability-implicit-bool-conversion, -readability-isolate-declaration, -readability-magic-numbers, @@ -78,9 +89,7 @@ Checks: >- -readability-redundant-string-init, -readability-uppercase-literal-suffix, -readability-use-anyofallof, - -warnings-as-errors WarningsAsErrors: '*' -HeaderFilterRegex: '^.*/src/esphome/.*' AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: @@ -104,6 +113,10 @@ CheckOptions: value: llvm - key: modernize-use-nullptr.NullMacros value: 'NULL' + - key: modernize-make-unique.MakeSmartPtrFunction + value: 'make_unique' + - key: modernize-make-unique.MakeSmartPtrFunctionHeader + value: 'esphome/core/helpers.h' - key: readability-identifier-naming.LocalVariableCase value: 'lower_case' - key: readability-identifier-naming.ClassCase @@ -117,15 +130,19 @@ CheckOptions: - key: readability-identifier-naming.StaticConstantCase value: 'UPPER_CASE' - key: readability-identifier-naming.StaticVariableCase - value: 'UPPER_CASE' + value: 'lower_case' - key: readability-identifier-naming.GlobalConstantCase value: 'UPPER_CASE' - key: readability-identifier-naming.ParameterCase value: 'lower_case' - - key: readability-identifier-naming.PrivateMemberPrefix - value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED' - - key: readability-identifier-naming.PrivateMethodPrefix - value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED' + - key: readability-identifier-naming.PrivateMemberCase + value: 'lower_case' + - key: readability-identifier-naming.PrivateMemberSuffix + value: '_' + - key: readability-identifier-naming.PrivateMethodCase + value: 'lower_case' + - key: readability-identifier-naming.PrivateMethodSuffix + value: '_' - key: readability-identifier-naming.ClassMemberCase value: 'lower_case' - key: readability-identifier-naming.ClassMemberCase diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8f7d751437..433e5d2792 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,29 @@ { "name": "ESPHome Dev", - "context": "..", - "dockerFile": "../docker/Dockerfile.dev", - "postCreateCommand": "mkdir -p config && pip3 install -e .", - "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], + "image": "esphome/esphome-lint:dev", + "postCreateCommand": [ + "script/devcontainer-post-create" + ], + "runArgs": [ + "--privileged", + "-e", + "ESPHOME_DASHBOARD_USE_PING=1" + ], "appPort": 6052, "extensions": [ + // python "ms-python.python", "visualstudioexptteam.vscodeintellicode", - "redhat.vscode-yaml" + // yaml + "redhat.vscode-yaml", + // cpp + "ms-vscode.cpptools", + // editorconfig + "editorconfig.editorconfig", ], "settings": { - "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.pythonPath": "/usr/bin/python3", "python.linting.pylintEnabled": true, "python.linting.enabled": true, "python.formatting.provider": "black", @@ -19,7 +31,7 @@ "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "terminal.integrated.shell.linux": "/bin/bash", + "terminal.integrated.defaultProfile.linux": "bash", "yaml.customTags": [ "!secret scalar", "!lambda scalar", @@ -27,6 +39,18 @@ "!include_dir_list scalar", "!include_dir_merge_list scalar", "!include_dir_merge_named scalar" - ] + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true + }, + "files.associations": { + "**/.vscode/*.json": "jsonc" + }, + "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", } } diff --git a/.dockerignore b/.dockerignore index e1baed38ca..9f14b98059 100644 --- a/.dockerignore +++ b/.dockerignore @@ -103,6 +103,10 @@ venv.bak/ # mypy .mypy_cache/ +# PlatformIO +.pio/ + +# ESPHome config/ examples/ Dockerfile diff --git a/.editorconfig b/.editorconfig index 29cbb1e32f..8ccf1eeebc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ insert_final_newline = true charset = utf-8 # python -[*.{py}] +[*.py] indent_style = space indent_size = 4 @@ -25,4 +25,10 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 -quote_type = single \ No newline at end of file +quote_type = single + +# JSON +[*.json] +indent_style = space +indent_size = 2 + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..dad0966222 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize line endings to LF in the repository +* text eol=lf diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 225c029bd9..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 - -# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) -onlyLabels: [] - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - not-stale - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: true - -# Set to true to ignore issues with an assignee (defaults to false) -exemptAssignees: false - -# Label to use when marking as stale -staleLabel: stale - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. - -# Comment to post when removing the stale label. -# unmarkComment: > -# Your comment here. - -# Comment to post when closing a stale Issue or Pull Request. -# closeComment: > -# Your comment here. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 10 - -# Limit to only `issues` or `pulls` -only: pulls - -# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': -# pulls: -# daysUntilStale: 30 -# markComment: > -# This pull request has been automatically marked as stale because it has not had -# recent activity. It will be closed if no further activity occurs. Thank you -# for your contributions. - -# issues: -# exemptLabels: -# - confirmed diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index b5f8b7b0e0..12f5a7dfc2 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -3,53 +3,47 @@ name: CI for docker images # Only run when docker paths change on: push: - branches: [dev, beta, master] + branches: [dev, beta, release] paths: - 'docker/**' - '.github/workflows/**' + - 'requirements*.txt' + - 'platformio.ini' pull_request: paths: - 'docker/**' - '.github/workflows/**' + - 'requirements*.txt' + - 'platformio.ini' jobs: check-docker: name: Build docker containers runs-on: ubuntu-latest strategy: - fail-fast: false matrix: arch: [amd64, armv7, aarch64] - build_type: ["hassio", "docker"] + build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 - - name: Set up env variables - run: | - base_version="3.4.0" + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi + - name: Set TAG + run: | + echo "TAG=check" >> $GITHUB_ENV - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:dev" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=ci" \ - --cache-from "${BUILD_TO}:dev" \ - --file "${DOCKERFILE}" \ - . + - name: Run build + run: | + docker/build.py \ + --tag "${TAG}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a27cbe67b0..45e2f2735c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,158 +4,153 @@ name: CI on: push: - # On dev branch release-dev already performs CI checks - # On other branches the `pull_request` trigger will be used - branches: [beta, master] + branches: [dev, beta, release] pull_request: jobs: - lint-clang-format: + ci: + name: ${{ matrix.name }} runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files strategy: fail-fast: false matrix: - split: [1, 2, 3, 4] - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom + include: + - id: ci-custom + name: Run script/ci-custom + - id: lint-python + name: Run script/lint-python + - id: test + file: tests/test1.yaml + name: Test tests/test1.yaml + pio_cache_key: test1 + - id: test + file: tests/test2.yaml + name: Test tests/test2.yaml + pio_cache_key: test2 + - id: test + file: tests/test3.yaml + name: Test tests/test3.yaml + pio_cache_key: test1 + - id: test + file: tests/test4.yaml + name: Test tests/test4.yaml + pio_cache_key: test4 + - id: test + file: tests/test5.yaml + name: Test tests/test5.yaml + pio_cache_key: test5 + - id: pytest + name: Run pytest + - id: clang-format + name: Run script/clang-format + - id: clang-tidy + name: Run script/clang-tidy for ESP8266 + options: --environment esp8266-tidy --grep USE_ESP8266 + pio_cache_key: tidyesp8266 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 1/4 + options: --environment esp32-tidy --split-num 4 --split-at 1 + pio_cache_key: tidyesp32 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 2/4 + options: --environment esp32-tidy --split-num 4 --split-at 2 + pio_cache_key: tidyesp32 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 3/4 + options: --environment esp32-tidy --split-num 4 --split-at 3 + pio_cache_key: tidyesp32 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 4/4 + options: --environment esp32-tidy --split-num 4 --split-at 4 + pio_cache_key: tidyesp32 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 esp-idf + options: --environment esp32-idf-tidy --grep USE_ESP_IDF + pio_cache_key: tidyesp32-idf - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - - name: Run clang-tidy - run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} - - name: Suggest changes - run: script/ci-suggest-changes - - lint-python: - # Don't use the esphome-lint docker image because it may contain outdated requirements. - # This way, all dependencies are cached via the cache action. - runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 + id: python with: python-version: '3.7' + - name: Cache pip modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + key: pip-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} restore-keys: | - esphome-pip-3.7- + pip-${{ steps.python.outputs.python-version }}- + - name: Set up python environment - run: script/setup + run: | + pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip3 install -e . + + # Use per check platformio cache because checks use different parts + - name: Cache platformio + uses: actions/cache@v2 + with: + path: ~/.platformio + key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} + if: matrix.id == 'test' || matrix.id == 'clang-tidy' + + - name: Install clang tools + run: | + sudo apt-get install \ + clang-format-11 \ + clang-tidy-11 + if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/ci-custom.json" echo "::add-matcher::.github/workflows/matchers/lint-python.json" echo "::add-matcher::.github/workflows/matchers/python.json" + echo "::add-matcher::.github/workflows/matchers/pytest.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + - name: Lint Custom - run: script/ci-custom.py + run: | + script/ci-custom.py + script/build_codeowners.py --check + if: matrix.id == 'ci-custom' + - name: Lint Python run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check + if: matrix.id == 'lint-python' - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup + - run: esphome compile ${{ matrix.file }} + if: matrix.id == 'test' + env: + # Also cache libdeps, store them in a ~/.platformio subfolder + PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - name: Run pytest run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests + pytest -vv --tb=native tests + if: matrix.id == 'pytest' + + # Also run git-diff-index so that the step is marked as failed on formatting errors, + # since clang-format doesn't do anything but change files if -i is passed. + - name: Run clang-format + run: | + script/clang-format -i + git diff-index --quiet HEAD -- + if: matrix.id == 'clang-format' + + - name: Run clang-tidy + run: | + script/clang-tidy --all-headers --fix ${{ matrix.options }} + if: matrix.id == 'clang-tidy' + env: + # Also cache libdeps, store them in a ~/.platformio subfolder + PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + + - name: Suggested changes + run: script/ci-suggest-changes + if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format') diff --git a/.github/workflows/docker-lint-build.yml b/.github/workflows/docker-lint-build.yml deleted file mode 100644 index d254ac332a..0000000000 --- a/.github/workflows/docker-lint-build.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build and publish lint docker image - -# Only run when docker paths change -on: - push: - branches: [dev] - paths: - - 'docker/Dockerfile.lint' - - 'requirements.txt' - - 'requirements_optional.txt' - - 'requirements_test.txt' - - 'platformio.ini' - - '.github/workflows/docker-lint-build.yml' - -jobs: - publish-docker-lint-iage: - name: Build docker containers - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - echo "TAG=1.1" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "esphome/esphome-lint:latest" || true - - name: Build - run: | - docker build \ - --cache-from "esphome/esphome-lint:latest" \ - --file "docker/Dockerfile.lint" \ - --tag "esphome/esphome-lint:latest" \ - --tag "esphome/esphome-lint:${TAG}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: | - docker push "esphome/esphome-lint:${TAG}" - docker push "esphome/esphome-lint:latest" diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000000..375b8f1db4 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,21 @@ +name: Lock + +on: + schedule: + - cron: '30 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + github-token: ${{ github.token }} + pr-lock-inactive-days: "1" + pr-lock-reason: "" + process-only: prs diff --git a/.github/workflows/matchers/pytest.json b/.github/workflows/matchers/pytest.json new file mode 100644 index 0000000000..0eb8f050e6 --- /dev/null +++ b/.github/workflows/matchers/pytest.json @@ -0,0 +1,19 @@ +{ + "problemMatcher": [ + { + "owner": "pytest", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^\\s+File \"(.*)\", line (\\d+), in (.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s+(.*)$", + "message": 1 + } + ] + } + ] +} diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml deleted file mode 100644 index f8b90d524f..0000000000 --- a/.github/workflows/release-dev.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: Publish dev releases to docker hub - -on: - push: - branches: - - dev - -jobs: - # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - - lint-clang-format: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files - strategy: - fail-fast: false - matrix: - split: [1, 2, 3, 4] - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - - name: Run clang-tidy - run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} - - name: Suggest changes - run: script/ci-suggest-changes - - lint-python: - # Don't use the esphome-lint docker image because it may contain outdated requirements. - # This way, all dependencies are cached via the cache action. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up python environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Lint Custom - run: script/ci-custom.py - - name: Lint Python - run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup - - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Run pytest - run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests - - deploy-docker: - name: Build and publish docker containers - if: github.repository == 'esphome/esphome' - runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] - strategy: - matrix: - arch: [amd64, armv7, aarch64] - # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly - build_type: ["docker"] - steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - TAG="${GITHUB_SHA:0:7}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Set up env variables - run: | - base_version="3.4.0" - - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi - - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:dev" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --tag "${BUILD_TO}:dev" \ - --cache-from "${BUILD_TO}:dev" \ - --file "${DOCKERFILE}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: | - docker push "${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:dev" - - - deploy-docker-manifest: - if: github.repository == 'esphome/esphome' - runs-on: ubuntu-latest - needs: [deploy-docker] - steps: - - name: Enable experimental manifest support - run: | - mkdir -p ~/.docker - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - name: Set TAG - run: | - TAG="${GITHUB_SHA:0:7}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - name: "Create the manifest" - run: | - docker manifest create esphome/esphome:${TAG} \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:${TAG} - - docker manifest create esphome/esphome:dev \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9523a2164c..afd893d065 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,164 +1,35 @@ name: Publish Release on: + workflow_dispatch: release: types: [published] + schedule: + - cron: "0 2 * * *" jobs: - # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - - lint-clang-format: + init: + name: Initialize build runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 + outputs: + tag: ${{ steps.tag.outputs.tag }} steps: - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files - strategy: - fail-fast: false - matrix: - split: [1, 2, 3, 4] - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - - name: Register problem matchers + - name: Get tag + id: tag run: | - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - - name: Run clang-tidy - run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} - - name: Suggest changes - run: script/ci-suggest-changes - - lint-python: - # Don't use the esphome-lint docker image because it may contain outdated requirements. - # This way, all dependencies are cached via the cache action. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up python environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Lint Custom - run: script/ci-custom.py - - name: Lint Python - run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Run pytest - run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests + if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then + TAG="${GITHUB_REF#refs/tags/}" + else + TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") + today="$(date --utc '+%Y%m%d')" + TAG="${TAG}${today}" + fi + echo "::set-output name=tag::${TAG}" deploy-pypi: name: Build and publish to PyPi - if: github.repository == 'esphome/esphome' - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -182,129 +53,93 @@ jobs: name: Build and publish docker containers if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + needs: [init] strategy: matrix: arch: [amd64, armv7, aarch64] - build_type: ["hassio", "docker"] + build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - TAG="${GITHUB_REF#refs/tags/v}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Set up env variables - run: | - base_version="3.4.0" + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 - if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then - cache_tag="beta" - else - cache_tag="latest" - fi + - name: Log in to docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - # Set env variables so these values don't need to be calculated again - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - echo "CACHE_TAG=${cache_tag}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:${CACHE_TAG}" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --cache-from "${BUILD_TO}:${CACHE_TAG}" \ - --file "${DOCKERFILE}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: docker push "${BUILD_TO}:${TAG}" - - # Always publish to beta tag (also full releases) - - name: Publish docker beta tag - run: | - docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" - docker push "${BUILD_TO}:beta" - - - if: ${{ !github.event.release.prerelease }} - name: Publish docker latest tag - run: | - docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" - docker push "${BUILD_TO}:latest" + - name: Build and push + run: | + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + build \ + --push deploy-docker-manifest: if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: [init, deploy-docker] + strategy: + matrix: + build_type: ["ha-addon", "docker", "lint"] steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' - name: Enable experimental manifest support run: | mkdir -p ~/.docker echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - name: Set TAG - run: | - TAG="${GITHUB_REF#refs/tags/v}" - echo "TAG=${TAG}" >> $GITHUB_ENV + - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - name: "Create the manifest" - run: | - docker manifest create esphome/esphome:${TAG} \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:${TAG} + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Publish docker beta tag + - name: Run manifest run: | - docker manifest create esphome/esphome:beta \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:beta - - - name: Publish docker latest tag - if: ${{ !github.event.release.prerelease }} - run: | - docker manifest create esphome/esphome:latest \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:latest + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --build-type "${{ matrix.build_type }}" \ + manifest deploy-hassio-repo: - if: github.repository == 'esphome/esphome' + if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest needs: [deploy-docker] steps: - env: TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} run: | - TAG="${GITHUB_REF#refs/tags/v}" + TAG="${GITHUB_REF#refs/tags/}" curl \ -u ":$TOKEN" \ -X POST \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ - -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}" + -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..712ae1a289 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: Stale + +on: + schedule: + - cron: '30 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + repo-token: ${{ github.token }} + days-before-pr-stale: 90 + days-before-pr-close: 7 + days-before-issue-stale: -1 + days-before-issue-close: -1 + remove-stale-when-updated: true + stale-pr-label: "stale" + exempt-pr-labels: "no-stale" + stale-pr-message: > + There hasn't been any activity on this pull request recently. This + pull request has been automatically marked as stale because of that + and will be closed if no further activity occurs within 7 days. + Thank you for your contributions. diff --git a/.gitignore b/.gitignore index a24550ad54..57b8478bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ __pycache__/ # Intellij Idea .idea +# Vim +*.swp + # Hide some OS X stuff .DS_Store .AppleDouble @@ -99,10 +102,7 @@ CMakeLists.txt .idea/**/dynamic.xml # CMake -cmake-build-debug/ -cmake-build-livingroom8266/ -cmake-build-livingroom32/ -cmake-build-release/ +cmake-build-*/ CMakeCache.txt CMakeFiles @@ -122,4 +122,8 @@ config/ tests/build/ tests/.esphome/ /.temp-clang-tidy.cpp +/.temp/ .pio/ + +sdkconfig.* +!sdkconfig.defaults diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e56aa17cf..a821c21fa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,5 +23,5 @@ repos: - id: no-commit-to-branch args: - --branch=dev - - --branch=master + - --branch=release - --branch=beta diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 513228722a..b6584bc735 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,11 +1,32 @@ { - "version": "2.0.0", - "tasks": [ + "version": "2.0.0", + "tasks": [ + { + "label": "run", + "type": "shell", + "command": "python3 -m esphome dashboard config/", + "problemMatcher": [] + }, + { + "label": "clang-tidy", + "type": "shell", + "command": "./script/clang-tidy", + "problemMatcher": [ { - "label": "run", - "type": "shell", - "command": "python3 -m esphome dashboard config/", - "problemMatcher": [] + "owner": "clang-tidy", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] } - ] + ] + } + ] } diff --git a/CODEOWNERS b/CODEOWNERS index bb29b114e8..7b16959b87 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,31 +14,46 @@ esphome/core/* @esphome/core esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/addressable_light/* @justfalter +esphome/components/airthings_ble/* @jeromelaban +esphome/components/airthings_wave_mini/* @ncareau +esphome/components/airthings_wave_plus/* @jeromelaban +esphome/components/am43/* @buxtronix +esphome/components/am43/cover/* @buxtronix esphome/components/animation/* @syndlex +esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron +esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter +esphome/components/ccs811/* @habbie esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet +esphome/components/color_temperature/* @jesserockz esphome/components/coolix/* @glmnet esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/ct_clamp/* @jesserockz +esphome/components/current_based/* @djwmarcx +esphome/components/daly_bms/* @s1lvi0 +esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee +esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_controller/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_improv/* @jesserockz +esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter @@ -46,7 +61,14 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle +esphome/components/graph/* @synco +esphome/components/havells_solar/* @sourabhjaiswal +esphome/components/hbridge/fan/* @WeekendWarrior +esphome/components/hbridge/light/* @DotNetDann +esphome/components/heatpumpir/* @rob-deutsch +esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter +esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv/* @jesserockz esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -57,6 +79,7 @@ esphome/components/json/* @OttoWinter esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/logger/* @esphome/core +esphome/components/ltr390/* @sjtrny esphome/components/max7219digit/* @rspaargaren esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz @@ -67,33 +90,58 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp9808/* @k7hpn -esphome/components/midea_ac/* @dudanov -esphome/components/midea_dongle/* @dudanov +esphome/components/mdns/* @esphome/core +esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey +esphome/components/modbus_controller/* @martgras +esphome/components/modbus_controller/binary_sensor/* @martgras +esphome/components/modbus_controller/number/* @martgras +esphome/components/modbus_controller/output/* @martgras +esphome/components/modbus_controller/sensor/* @martgras +esphome/components/modbus_controller/switch/* @martgras +esphome/components/modbus_controller/text_sensor/* @martgras esphome/components/network/* @esphome/core +esphome/components/nextion/* @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/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter +esphome/components/pipsolar/* @andreashergert1984 +esphome/components/pm1006/* @habbie +esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core +esphome/components/preferences/* @esphome/core esphome/components/pulse_meter/* @stevebaxter +esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz +esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet +esphome/components/safe_mode/* @paulmonigatti +esphome/components/scd4x/* @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces +esphome/components/sdp3x/* @Azimath +esphome/components/selec_meter/* @sourabhjaiswal +esphome/components/select/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/socket/* @esphome/core esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 @@ -108,17 +156,23 @@ esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_spi/* @kbx81 esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 +esphome/components/st7920/* @marsjan155 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter +esphome/components/tlc5947/* @rnauber esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage +esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka +esphome/components/toshiba/* @kbx81 +esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43d5e79074..e96bb5745b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,6 @@ # Contributing to ESPHome -This python project is responsible for reading in YAML configuration files, -converting them to C++ code. This code is then converted to a platformio project and compiled -with [esphome-core](https://github.com/esphome/esphome-core), the C++ framework behind the project. - -For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphomeyaml +For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome Things to note when contributing: diff --git a/README.md b/README.md index f21e748d40..bb6fb37d3a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ESPHome [![Build Status](https://travis-ci.org/esphome/esphome.svg?branch=master)](https://travis-ci.org/esphome/esphome) [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) +# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0d126a2944..e66c3e1d95 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,60 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:3.4.0 -FROM ${BUILD_FROM} +# Build these with the build.py script +# Example: +# python3 docker/build.py --tag dev --arch amd64 --build-type docker build + +# One of "docker", "hassio" +ARG BASEIMGTYPE=docker + +FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7 +FROM debian:bullseye-20210902-slim AS base-docker-amd64 +FROM debian:bullseye-20210902-slim AS base-docker-arm64 +FROM debian:bullseye-20210902-slim AS base-docker-armv7 + +# Use TARGETARCH/TARGETVARIANT defined by docker +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +FROM base-${BASEIMGTYPE}-${TARGETARCH}${TARGETVARIANT} AS base + +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 \ + python3-setuptools=52.0.0-4 \ + python3-pil=8.1.2+dfsg-0.3 \ + python3-cryptography=3.3.2-1 \ + iputils-ping=3:20210202-1 \ + git=1:2.30.2-1 \ + curl=7.74.0-1.3+b1 \ + && rm -rf \ + /tmp/* \ + /var/{cache,log}/* \ + /var/lib/apt/lists/* + +ENV \ + # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/ + LANG=C.UTF-8 LC_ALL=C.UTF-8 \ + # Store globally installed pio libs in /piolibs + PLATFORMIO_GLOBALLIB_DIR=/piolibs + +RUN \ + # Ubuntu python3-pip is missing wheel + pip3 install --no-cache-dir \ + wheel==0.36.2 \ + platformio==5.2.0 \ + # Change some platformio settings + && platformio settings set enable_telemetry No \ + && platformio settings set check_libraries_interval 1000000 \ + && platformio settings set check_platformio_interval 1000000 \ + && platformio settings set check_platforms_interval 1000000 \ + && mkdir -p /piolibs + + + +# ======================= docker-type image ======================= +FROM base AS docker # First install requirements to leverage caching when requirements don't change COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / @@ -7,9 +62,9 @@ RUN \ pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ && /platformio_install_deps.py /platformio.ini -# Then copy esphome and install -COPY . . -RUN pip3 install --no-cache-dir -e . +# Copy esphome and install +COPY . /esphome +RUN pip3 install --no-cache-dir -e /esphome # Settings for dashboard ENV USERNAME="" PASSWORD="" @@ -17,14 +72,85 @@ ENV USERNAME="" PASSWORD="" # Expose the dashboard to Docker EXPOSE 6052 -# Run healthcheck (heartbeat) -HEALTHCHECK --interval=30s --timeout=30s \ - CMD curl --fail http://localhost:6052 || exit 1 +COPY docker/docker_entrypoint.sh /entrypoint.sh # The directory the user should mount their configuration files to +VOLUME /config WORKDIR /config -# Set entrypoint to esphome so that the user doesn't have to type 'esphome' +# Set entrypoint to esphome (via a script) so that the user doesn't have to type 'esphome' # in every docker command twice -ENTRYPOINT ["esphome"] +ENTRYPOINT ["/entrypoint.sh"] # When no arguments given, start the dashboard in the workdir CMD ["dashboard", "/config"] + + + + +# ======================= hassio-type image ======================= +FROM base AS hassio + +RUN \ + apt-get update \ + # Use pinned versions so that we get updates with build caching + && apt-get install -y --no-install-recommends \ + nginx=1.18.0-6.1 \ + && rm -rf \ + /tmp/* \ + /var/{cache,log}/* \ + /var/lib/apt/lists/* + +ARG BUILD_VERSION=dev + +# Copy root filesystem +COPY docker/hassio-rootfs/ / + +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt requirements_optional.txt docker/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 + +# Copy esphome and install +COPY . /esphome +RUN pip3 install --no-cache-dir -e /esphome + +# Labels +LABEL \ + io.hass.name="ESPHome" \ + io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \ + io.hass.type="addon" \ + io.hass.version="${BUILD_VERSION}" + # io.hass.arch is inherited from addon-debian-base + + + + +# ======================= lint-type image ======================= +FROM base AS lint + +ENV \ + PLATFORMIO_CORE_DIR=/esphome/.temp/platformio + +RUN \ + apt-get update \ + # Use pinned versions so that we get updates with build caching + && apt-get install -y --no-install-recommends \ + clang-format-11=1:11.0.1-2 \ + clang-tidy-11=1:11.0.1-2 \ + patch=2.7.6-7 \ + software-properties-common=0.96.20.2-2.1 \ + nano=5.4-2 \ + build-essential=12.9 \ + python3-dev=3.9.2-3 \ + && rm -rf \ + /tmp/* \ + /var/{cache,log}/* \ + /var/lib/apt/lists/* + +COPY requirements.txt requirements_optional.txt docker/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 + +VOLUME ["/esphome"] +WORKDIR /esphome diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev deleted file mode 100644 index ebcf14d1bc..0000000000 --- a/docker/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM esphome/esphome-base-amd64:3.4.0 - -COPY . . - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - python3-wheel \ - net-tools \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspaces -ENV SHELL /bin/bash diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio deleted file mode 100644 index 5dd9339b18..0000000000 --- a/docker/Dockerfile.hassio +++ /dev/null @@ -1,25 +0,0 @@ -ARG BUILD_FROM -FROM ${BUILD_FROM} - -# First install requirements to leverage caching when requirements don't change -COPY requirements.txt requirements_optional.txt docker/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 - -# Copy root filesystem -COPY docker/rootfs/ / - -# Then copy esphome and install -COPY . /opt/esphome/ -RUN pip3 install --no-cache-dir -e /opt/esphome - -# Build arguments -ARG BUILD_VERSION=dev - -# Labels -LABEL \ - io.hass.name="ESPHome" \ - io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \ - io.hass.type="addon" \ - io.hass.version=${BUILD_VERSION} diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint deleted file mode 100644 index 60d63152a0..0000000000 --- a/docker/Dockerfile.lint +++ /dev/null @@ -1,9 +0,0 @@ -FROM esphome/esphome-lint-base:3.4.0 - -COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / -RUN \ - pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ - && /platformio_install_deps.py /platformio.ini - -VOLUME ["/esphome"] -WORKDIR /esphome diff --git a/docker/build.py b/docker/build.py new file mode 100755 index 0000000000..1157d8287a --- /dev/null +++ b/docker/build.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +from dataclasses import dataclass +import subprocess +import argparse +from platform import machine +import shlex +import re +import sys + + +CHANNEL_DEV = 'dev' +CHANNEL_BETA = 'beta' +CHANNEL_RELEASE = 'release' +CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] + +ARCH_AMD64 = 'amd64' +ARCH_ARMV7 = 'armv7' +ARCH_AARCH64 = 'aarch64' +ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] + +TYPE_DOCKER = 'docker' +TYPE_HA_ADDON = 'ha-addon' +TYPE_LINT = 'lint' +TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] + + +parser = argparse.ArgumentParser() +parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") +parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") +parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") +parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") +subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) +build_parser = subparsers.add_parser("build", help="Build the image") +build_parser.add_argument("--push", help="Also push the images", action="store_true") +manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") + + +@dataclass(frozen=True) +class DockerParams: + build_to: str + manifest_to: str + baseimgtype: str + platform: str + target: str + + @classmethod + def for_type_arch(cls, build_type, arch): + prefix = { + TYPE_DOCKER: "esphome/esphome", + TYPE_HA_ADDON: "esphome/esphome-hassio", + TYPE_LINT: "esphome/esphome-lint" + }[build_type] + build_to = f"{prefix}-{arch}" + baseimgtype = { + TYPE_DOCKER: "docker", + TYPE_HA_ADDON: "hassio", + TYPE_LINT: "docker", + }[build_type] + platform = { + ARCH_AMD64: "linux/amd64", + ARCH_ARMV7: "linux/arm/v7", + ARCH_AARCH64: "linux/arm64", + }[arch] + target = { + TYPE_DOCKER: "docker", + TYPE_HA_ADDON: "hassio", + TYPE_LINT: "lint", + }[build_type] + return cls( + build_to=build_to, + manifest_to=prefix, + baseimgtype=baseimgtype, + platform=platform, + target=target, + ) + + +def main(): + args = parser.parse_args() + + def run_command(*cmd, ignore_error: bool = False): + print(f"$ {shlex.join(list(cmd))}") + if not args.dry_run: + rc = subprocess.call(list(cmd)) + if rc != 0 and not ignore_error: + print("Command failed") + sys.exit(1) + + # detect channel from tag + match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag) + if match is None: + channel = CHANNEL_DEV + elif match.group(1) is None: + channel = CHANNEL_RELEASE + else: + channel = CHANNEL_BETA + + tags_to_push = [args.tag] + if channel == CHANNEL_DEV: + tags_to_push.append("dev") + elif channel == CHANNEL_BETA: + tags_to_push.append("beta") + elif channel == CHANNEL_RELEASE: + # Additionally push to beta + tags_to_push.append("beta") + tags_to_push.append("latest") + + if args.command == "build": + # 1. pull cache image + params = DockerParams.for_type_arch(args.build_type, args.arch) + cache_tag = { + CHANNEL_DEV: "cache-dev", + CHANNEL_BETA: "cache-beta", + CHANNEL_RELEASE: "cache-latest", + }[channel] + cache_img = f"ghcr.io/{params.build_to}:{cache_tag}" + + imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push] + imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push] + + # 3. build + cmd = [ + "docker", "buildx", "build", + "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", + "--build-arg", f"BUILD_VERSION={args.tag}", + "--cache-from", f"type=registry,ref={cache_img}", + "--file", "docker/Dockerfile", + "--platform", params.platform, + "--target", params.target, + ] + for img in imgs: + cmd += ["--tag", img] + if args.push: + cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"] + + run_command(*cmd, ".") + elif args.command == "manifest": + manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to + + targets = [f"{manifest}:{tag}" for tag in tags_to_push] + targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push] + # 1. Create manifests + for target in targets: + cmd = ["docker", "manifest", "create", target] + for arch in ARCHS: + src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}" + if target.startswith("ghcr.io"): + src = f"ghcr.io/{src}" + cmd.append(src) + run_command(*cmd) + # 2. Push manifests + for target in targets: + run_command( + "docker", "manifest", "push", target + ) + + +if __name__ == "__main__": + main() diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh new file mode 100755 index 0000000000..75d5e0b7b5 --- /dev/null +++ b/docker/docker_entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# If /cache is mounted, use that as PIO's coredir +# otherwise use path in /config (so that PIO packages aren't downloaded on each compile) + +if [[ -d /cache ]]; then + pio_cache_base=/cache/platformio +else + pio_cache_base=/config/.esphome/platformio +fi + +if [[ ! -d "${pio_cache_base}" ]]; then + echo "Creating cache directory ${pio_cache_base}" + echo "You can change this behavior by mounting a directory to the container's /cache directory." + mkdir -p "${pio_cache_base}" +fi + +# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` +# setting `core_dir` would therefore prevent pio from accessing +export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" +export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" +export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" + +exec esphome "$@" diff --git a/docker/rootfs/etc/cont-init.d/10-requirements.sh b/docker/hassio-rootfs/etc/cont-init.d/10-requirements.sh similarity index 100% rename from docker/rootfs/etc/cont-init.d/10-requirements.sh rename to docker/hassio-rootfs/etc/cont-init.d/10-requirements.sh diff --git a/docker/rootfs/etc/cont-init.d/20-nginx.sh b/docker/hassio-rootfs/etc/cont-init.d/20-nginx.sh similarity index 100% rename from docker/rootfs/etc/cont-init.d/20-nginx.sh rename to docker/hassio-rootfs/etc/cont-init.d/20-nginx.sh diff --git a/docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh b/docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh new file mode 100644 index 0000000000..1073a2fa45 --- /dev/null +++ b/docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh @@ -0,0 +1,9 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Community Hass.io Add-ons: ESPHome +# This files creates all directories used by esphome +# ============================================================================== + +pio_cache_base=/data/cache/platformio + +mkdir -p "${pio_cache_base}" diff --git a/docker/rootfs/etc/nginx/includes/mime.types b/docker/hassio-rootfs/etc/nginx/includes/mime.types similarity index 100% rename from docker/rootfs/etc/nginx/includes/mime.types rename to docker/hassio-rootfs/etc/nginx/includes/mime.types diff --git a/docker/rootfs/etc/nginx/includes/proxy_params.conf b/docker/hassio-rootfs/etc/nginx/includes/proxy_params.conf similarity index 100% rename from docker/rootfs/etc/nginx/includes/proxy_params.conf rename to docker/hassio-rootfs/etc/nginx/includes/proxy_params.conf diff --git a/docker/rootfs/etc/nginx/includes/server_params.conf b/docker/hassio-rootfs/etc/nginx/includes/server_params.conf similarity index 100% rename from docker/rootfs/etc/nginx/includes/server_params.conf rename to docker/hassio-rootfs/etc/nginx/includes/server_params.conf diff --git a/docker/rootfs/etc/nginx/includes/ssl_params.conf b/docker/hassio-rootfs/etc/nginx/includes/ssl_params.conf similarity index 100% rename from docker/rootfs/etc/nginx/includes/ssl_params.conf rename to docker/hassio-rootfs/etc/nginx/includes/ssl_params.conf diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/hassio-rootfs/etc/nginx/nginx.conf similarity index 100% rename from docker/rootfs/etc/nginx/nginx.conf rename to docker/hassio-rootfs/etc/nginx/nginx.conf diff --git a/docker/rootfs/etc/nginx/servers/direct-ssl.disabled b/docker/hassio-rootfs/etc/nginx/servers/direct-ssl.disabled similarity index 100% rename from docker/rootfs/etc/nginx/servers/direct-ssl.disabled rename to docker/hassio-rootfs/etc/nginx/servers/direct-ssl.disabled diff --git a/docker/rootfs/etc/nginx/servers/direct.disabled b/docker/hassio-rootfs/etc/nginx/servers/direct.disabled similarity index 100% rename from docker/rootfs/etc/nginx/servers/direct.disabled rename to docker/hassio-rootfs/etc/nginx/servers/direct.disabled diff --git a/docker/rootfs/etc/nginx/servers/ingress.conf b/docker/hassio-rootfs/etc/nginx/servers/ingress.conf similarity index 100% rename from docker/rootfs/etc/nginx/servers/ingress.conf rename to docker/hassio-rootfs/etc/nginx/servers/ingress.conf diff --git a/docker/rootfs/etc/services.d/esphome/finish b/docker/hassio-rootfs/etc/services.d/esphome/finish similarity index 100% rename from docker/rootfs/etc/services.d/esphome/finish rename to docker/hassio-rootfs/etc/services.d/esphome/finish diff --git a/docker/rootfs/etc/services.d/esphome/run b/docker/hassio-rootfs/etc/services.d/esphome/run similarity index 66% rename from docker/rootfs/etc/services.d/esphome/run rename to docker/hassio-rootfs/etc/services.d/esphome/run index f806c50929..a0f20d63d6 100755 --- a/docker/rootfs/etc/services.d/esphome/run +++ b/docker/hassio-rootfs/etc/services.d/esphome/run @@ -22,5 +22,14 @@ if bashio::config.has_value 'relative_url'; then export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') fi +pio_cache_base=/data/cache/platformio +# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` +# setting `core_dir` would therefore prevent pio from accessing +export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" +export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" +export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" + +export PLATFORMIO_GLOBALLIB_DIR=/piolibs + bashio::log.info "Starting ESPHome dashboard..." exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/hassio-rootfs/etc/services.d/nginx/finish similarity index 100% rename from docker/rootfs/etc/services.d/nginx/finish rename to docker/hassio-rootfs/etc/services.d/nginx/finish diff --git a/docker/rootfs/etc/services.d/nginx/run b/docker/hassio-rootfs/etc/services.d/nginx/run similarity index 100% rename from docker/rootfs/etc/services.d/nginx/run rename to docker/hassio-rootfs/etc/services.d/nginx/run diff --git a/docker/platformio_install_deps.py b/docker/platformio_install_deps.py index 6f3e9f28d5..5625bd4d01 100755 --- a/docker/platformio_install_deps.py +++ b/docker/platformio_install_deps.py @@ -3,18 +3,11 @@ # all platformio libraries in the global storage import configparser -import re import subprocess import sys -config = configparser.ConfigParser() +config = configparser.ConfigParser(inline_comment_prefixes=(';', )) config.read(sys.argv[1]) -libs = [] -for line in config['common']['lib_deps'].splitlines(): - # Format: '1655@1.0.2 ; TinyGPSPlus (has name conflict)' (includes comment) - m = re.search(r'([a-zA-Z0-9-_/]+@[0-9\.]+)', line) - if m is None: - continue - libs.append(m.group(1)) +libs = [x for x in config['common']['lib_deps'].splitlines() if len(x) != 0] subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs]) diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh deleted file mode 100755 index d9a80cde2e..0000000000 --- a/docker/rootfs/etc/cont-init.d/30-esphome.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files installs the user ESPHome version if specified -# ============================================================================== - -declare esphome_version - -if bashio::config.has_value 'esphome_version'; then - esphome_version=$(bashio::config 'esphome_version') - if [[ $esphome_version == *":"* ]]; then - IFS=':' read -r -a array <<< "$esphome_version" - username=${array[0]} - ref=${array[1]} - else - username="esphome" - ref=$esphome_version - fi - full_url="https://github.com/${username}/esphome/archive/${ref}.zip" - bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." - pip3 install -U --no-cache-dir "${full_url}" \ - || bashio::exit.nok "Failed installing esphome pinned version." -fi diff --git a/docker/rootfs/etc/cont-init.d/40-migrate.sh b/docker/rootfs/etc/cont-init.d/40-migrate.sh deleted file mode 100755 index 88e8be26b9..0000000000 --- a/docker/rootfs/etc/cont-init.d/40-migrate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files migrates the esphome config directory from the old path -# ============================================================================== - -if [[ ! -d /config/esphome && -d /config/esphomeyaml ]]; then - echo "Moving config directory from /config/esphomeyaml to /config/esphome" - mv /config/esphomeyaml /config/esphome - mv /config/esphome/.esphomeyaml /config/esphome/.esphome -fi diff --git a/esphome/__main__.py b/esphome/__main__.py index 9af08a8f21..feb95e93c7 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -11,6 +11,7 @@ from esphome.config import iter_components, read_config, strip_default_ids from esphome.const import ( CONF_BAUD_RATE, CONF_BROKER, + CONF_DEASSERT_RTS_DTR, CONF_LOGGER, CONF_OTA, CONF_PASSWORD, @@ -71,7 +72,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api if default == "OTA": return CORE.address if show_mqtt and "mqtt" in CORE.config: - options.append(("MQTT ({})".format(CORE.config["mqtt"][CONF_BROKER]), "MQTT")) + options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) if default == "OTA": return "MQTT" if default is not None: @@ -99,10 +100,21 @@ def run_miniterm(config, port): baud_rate = config["logger"][CONF_BAUD_RATE] if baud_rate == 0: _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") + return _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) backtrace_state = False - with serial.Serial(port, baudrate=baud_rate) as ser: + ser = serial.Serial() + ser.baudrate = baud_rate + ser.port = port + + # We can't set to False by default since it leads to toggling and hence + # ESP32 resets on some platforms. + if config["logger"][CONF_DEASSERT_RTS_DTR]: + ser.dtr = False + ser.rts = False + + with ser: while True: try: raw = ser.readline() @@ -172,12 +184,30 @@ def compile_program(args, config): def upload_using_esptool(config, port): - path = CORE.firmware_bin + from esphome import platformio_api + first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( "upload_speed", 460800 ) def run_esptool(baud_rate): + idedata = platformio_api.get_idedata(config) + + firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" + flash_images = [ + platformio_api.FlashImage( + path=idedata.firmware_bin_path, + offset=firmware_offset, + ), + *idedata.extra_flash_images, + ] + + mcu = "esp8266" + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + + mcu = get_esp32_variant().lower() + cmd = [ "esptool.py", "--before", @@ -186,14 +216,15 @@ def upload_using_esptool(config, port): "hard_reset", "--baud", str(baud_rate), - "--chip", - "esp8266", "--port", port, + "--chip", + mcu, "write_flash", - "0x0", - path, + "-z", ] + for img in flash_images: + cmd += [img.offset, img.path] if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool @@ -217,11 +248,7 @@ def upload_using_esptool(config, port): def upload_program(config, args, host): # if upload is to a serial port use platformio, otherwise assume ota if get_port_type(host) == "SERIAL": - from esphome import platformio_api - - if CORE.is_esp8266: - return upload_using_esptool(config, host) - return platformio_api.run_upload(config, CORE.verbose, host) + return upload_using_esptool(config, host) from esphome import espota2 @@ -233,7 +260,7 @@ def upload_program(config, args, host): ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] - password = ota_conf[CONF_PASSWORD] + password = ota_conf.get(CONF_PASSWORD, "") return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) @@ -244,7 +271,7 @@ def show_logs(config, args, port): run_miniterm(config, port) return 0 if get_port_type(port) == "NETWORK" and "api" in config: - from esphome.api.client import run_logs + from esphome.components.api.client import run_logs return run_logs(config, port) if get_port_type(port) == "MQTT" and "mqtt" in config: @@ -284,7 +311,6 @@ def command_vscode(args): logging.disable(logging.INFO) logging.disable(logging.WARNING) - CORE.config_path = args.configuration vscode.read_config(args) @@ -404,30 +430,30 @@ def command_update_all(args): click.echo(f"{half_line}{middle_text}{half_line}") for f in files: - print("Updating {}".format(color(Fore.CYAN, f))) + print(f"Updating {color(Fore.CYAN, f)}") print("-" * twidth) print() rc = run_external_process( "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: - print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) + print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") success[f] = True else: - print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f)) + print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") success[f] = False print() print() print() - print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY"))) + print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") failed = 0 for f in files: if success[f]: - print(" - {}: {}".format(f, color(Fore.GREEN, "SUCCESS"))) + print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") else: - print(" - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED"))) + print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") failed += 1 return failed @@ -472,61 +498,9 @@ def parse_args(argv): metavar=("key", "value"), ) - # Keep backward compatibility with the old command line format of - # esphome . - # - # Unfortunately this can't be done by adding another configuration argument to the - # main config parser, as argparse is greedy when parsing arguments, so in regular - # usage it'll eat the command as the configuration argument and error out out - # because it can't parse the configuration as a command. - # - # Instead, construct an ad-hoc parser for the old format that doesn't actually - # process the arguments, but parses them enough to let us figure out if the old - # format is used. In that case, swap the command and configuration in the arguments - # and continue on with the normal parser (after raising a deprecation warning). - # - # Disable argparse's built-in help option and add it manually to prevent this - # parser from printing the help messagefor the old format when invoked with -h. - compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) - compat_parser.add_argument("-h", "--help") - compat_parser.add_argument("configuration", nargs="*") - compat_parser.add_argument( - "command", - choices=[ - "config", - "compile", - "upload", - "logs", - "run", - "clean-mqtt", - "wizard", - "mqtt-fingerprint", - "version", - "clean", - "dashboard", - ], - ) - - # on Python 3.9+ we can simply set exit_on_error=False in the constructor - def _raise(x): - raise argparse.ArgumentError(None, x) - - compat_parser.error = _raise - - try: - result, unparsed = compat_parser.parse_known_args(argv[1:]) - last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) - argv = argv[0:last_option] + [result.command] + result.configuration + unparsed - deprecated_argv_suggestion = argv - except argparse.ArgumentError: - # This is not an old-style command line, so we don't have to do anything. - deprecated_argv_suggestion = None - - # And continue on with regular parsing parser = argparse.ArgumentParser( description=f"ESPHome v{const.__version__}", parents=[options_parser] ) - parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) mqtt_options = argparse.ArgumentParser(add_help=False) mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") @@ -668,17 +642,91 @@ def parse_args(argv): ) parser_vscode = subparsers.add_parser("vscode") - parser_vscode.add_argument( - "configuration", help="Your YAML configuration file.", nargs=1 - ) + parser_vscode.add_argument("configuration", help="Your YAML configuration file.") parser_vscode.add_argument("--ace", action="store_true") parser_update = subparsers.add_parser("update-all") parser_update.add_argument( - "configuration", help="Your YAML configuration file directory.", nargs=1 + "configuration", help="Your YAML configuration file directories.", nargs="+" ) - return parser.parse_args(argv[1:]) + # Keep backward compatibility with the old command line format of + # esphome . + # + # Unfortunately this can't be done by adding another configuration argument to the + # main config parser, as argparse is greedy when parsing arguments, so in regular + # usage it'll eat the command as the configuration argument and error out out + # because it can't parse the configuration as a command. + # + # Instead, if parsing using the current format fails, construct an ad-hoc parser + # that doesn't actually process the arguments, but parses them enough to let us + # figure out if the old format is used. In that case, swap the command and + # configuration in the arguments and retry with the normal parser (and raise + # a deprecation warning). + arguments = argv[1:] + + # On Python 3.9+ we can simply set exit_on_error=False in the constructor + def _raise(x): + raise argparse.ArgumentError(None, x) + + # First, try new-style parsing, but don't exit in case of failure + try: + # duplicate parser so that we can use the original one to raise errors later on + current_parser = argparse.ArgumentParser(add_help=False, parents=[parser]) + current_parser.set_defaults(deprecated_argv_suggestion=None) + current_parser.error = _raise + return current_parser.parse_args(arguments) + except argparse.ArgumentError: + pass + + # Second, try compat parsing and rearrange the command-line if it succeeds + # Disable argparse's built-in help option and add it manually to prevent this + # parser from printing the help messagefor the old format when invoked with -h. + compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) + compat_parser.add_argument("-h", "--help", action="store_true") + compat_parser.add_argument("configuration", nargs="*") + compat_parser.add_argument( + "command", + choices=[ + "config", + "compile", + "upload", + "logs", + "run", + "clean-mqtt", + "wizard", + "mqtt-fingerprint", + "version", + "clean", + "dashboard", + "vscode", + "update-all", + ], + ) + + try: + compat_parser.error = _raise + result, unparsed = compat_parser.parse_known_args(argv[1:]) + last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration) + unparsed = [ + "--device" if arg in ("--upload-port", "--serial-port") else arg + for arg in unparsed + ] + arguments = ( + arguments[0:last_option] + + [result.command] + + result.configuration + + unparsed + ) + deprecated_argv_suggestion = arguments + except argparse.ArgumentError: + # old-style parsing failed, don't suggest any argument + deprecated_argv_suggestion = None + + # Finally, run the new-style parser again with the possibly swapped arguments, + # and let it error out if the command is unparsable. + parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) + return parser.parse_args(arguments) def run_esphome(argv): @@ -686,13 +734,13 @@ def run_esphome(argv): CORE.dashboard = args.dashboard setup_log(args.verbose, args.quiet) - if args.deprecated_argv_suggestion is not None: + if args.deprecated_argv_suggestion is not None and args.command != "vscode": _LOGGER.warning( "Calling ESPHome with the configuration before the command is deprecated " "and will be removed in the future. " ) _LOGGER.warning("Please instead use:") - _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion[1:])) + _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion)) if sys.version_info < (3, 7, 0): _LOGGER.error( diff --git a/esphome/api/api_pb2.py b/esphome/api/api_pb2.py deleted file mode 100644 index 6262b752c6..0000000000 --- a/esphome/api/api_pb2.py +++ /dev/null @@ -1,3997 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: api.proto - -import sys - -_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor.FileDescriptor( - name="api.proto", - package="", - syntax="proto3", - serialized_options=None, - serialized_pb=_b( - '\n\tapi.proto"#\n\x0cHelloRequest\x12\x13\n\x0b\x63lient_info\x18\x01 \x01(\t"Z\n\rHelloResponse\x12\x19\n\x11\x61pi_version_major\x18\x01 \x01(\r\x12\x19\n\x11\x61pi_version_minor\x18\x02 \x01(\r\x12\x13\n\x0bserver_info\x18\x03 \x01(\t""\n\x0e\x43onnectRequest\x12\x10\n\x08password\x18\x01 \x01(\t"+\n\x0f\x43onnectResponse\x12\x18\n\x10invalid_password\x18\x01 \x01(\x08"\x13\n\x11\x44isconnectRequest"\x14\n\x12\x44isconnectResponse"\r\n\x0bPingRequest"\x0e\n\x0cPingResponse"\x13\n\x11\x44\x65viceInfoRequest"\xad\x01\n\x12\x44\x65viceInfoResponse\x12\x15\n\ruses_password\x18\x01 \x01(\x08\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0bmac_address\x18\x03 \x01(\t\x12\x1c\n\x14\x65sphome_core_version\x18\x04 \x01(\t\x12\x18\n\x10\x63ompilation_time\x18\x05 \x01(\t\x12\r\n\x05model\x18\x06 \x01(\t\x12\x16\n\x0ehas_deep_sleep\x18\x07 \x01(\x08"\x15\n\x13ListEntitiesRequest"\x9a\x01\n ListEntitiesBinarySensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x14\n\x0c\x64\x65vice_class\x18\x05 \x01(\t\x12\x1f\n\x17is_status_binary_sensor\x18\x06 \x01(\x08"s\n\x19ListEntitiesCoverResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x15\n\ris_optimistic\x18\x05 \x01(\x08"\x90\x01\n\x17ListEntitiesFanResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1c\n\x14supports_oscillation\x18\x05 \x01(\x08\x12\x16\n\x0esupports_speed\x18\x06 \x01(\x08"\x8a\x02\n\x19ListEntitiesLightResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1b\n\x13supports_brightness\x18\x05 \x01(\x08\x12\x14\n\x0csupports_rgb\x18\x06 \x01(\x08\x12\x1c\n\x14supports_white_value\x18\x07 \x01(\x08\x12"\n\x1asupports_color_temperature\x18\x08 \x01(\x08\x12\x12\n\nmin_mireds\x18\t \x01(\x02\x12\x12\n\nmax_mireds\x18\n \x01(\x02\x12\x0f\n\x07\x65\x66\x66\x65\x63ts\x18\x0b \x03(\t"\xa3\x01\n\x1aListEntitiesSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x1b\n\x13unit_of_measurement\x18\x06 \x01(\t\x12\x19\n\x11\x61\x63\x63uracy_decimals\x18\x07 \x01(\x05"\x7f\n\x1aListEntitiesSwitchResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x12\n\noptimistic\x18\x06 \x01(\x08"o\n\x1eListEntitiesTextSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t"\x1a\n\x18ListEntitiesDoneResponse"\x18\n\x16SubscribeStatesRequest"7\n\x19\x42inarySensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"t\n\x12\x43overStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12-\n\x05state\x18\x02 \x01(\x0e\x32\x1e.CoverStateResponse.CoverState""\n\nCoverState\x12\x08\n\x04OPEN\x10\x00\x12\n\n\x06\x43LOSED\x10\x01"]\n\x10\x46\x61nStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x13\n\x0boscillating\x18\x03 \x01(\x08\x12\x18\n\x05speed\x18\x04 \x01(\x0e\x32\t.FanSpeed"\xa8\x01\n\x12LightStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x12\n\nbrightness\x18\x03 \x01(\x02\x12\x0b\n\x03red\x18\x04 \x01(\x02\x12\r\n\x05green\x18\x05 \x01(\x02\x12\x0c\n\x04\x62lue\x18\x06 \x01(\x02\x12\r\n\x05white\x18\x07 \x01(\x02\x12\x19\n\x11\x63olor_temperature\x18\x08 \x01(\x02\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\t \x01(\t"1\n\x13SensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x02"1\n\x13SwitchStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"5\n\x17TextSensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\t"\x98\x01\n\x13\x43overCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\x32\n\x07\x63ommand\x18\x03 \x01(\x0e\x32!.CoverCommandRequest.CoverCommand"-\n\x0c\x43overCommand\x12\x08\n\x04OPEN\x10\x00\x12\t\n\x05\x43LOSE\x10\x01\x12\x08\n\x04STOP\x10\x02"\x9d\x01\n\x11\x46\x61nCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x11\n\thas_speed\x18\x04 \x01(\x08\x12\x18\n\x05speed\x18\x05 \x01(\x0e\x32\t.FanSpeed\x12\x17\n\x0fhas_oscillating\x18\x06 \x01(\x08\x12\x13\n\x0boscillating\x18\x07 \x01(\x08"\x95\x03\n\x13LightCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x16\n\x0ehas_brightness\x18\x04 \x01(\x08\x12\x12\n\nbrightness\x18\x05 \x01(\x02\x12\x0f\n\x07has_rgb\x18\x06 \x01(\x08\x12\x0b\n\x03red\x18\x07 \x01(\x02\x12\r\n\x05green\x18\x08 \x01(\x02\x12\x0c\n\x04\x62lue\x18\t \x01(\x02\x12\x11\n\thas_white\x18\n \x01(\x08\x12\r\n\x05white\x18\x0b \x01(\x02\x12\x1d\n\x15has_color_temperature\x18\x0c \x01(\x08\x12\x19\n\x11\x63olor_temperature\x18\r \x01(\x02\x12\x1d\n\x15has_transition_length\x18\x0e \x01(\x08\x12\x19\n\x11transition_length\x18\x0f \x01(\r\x12\x18\n\x10has_flash_length\x18\x10 \x01(\x08\x12\x14\n\x0c\x66lash_length\x18\x11 \x01(\r\x12\x12\n\nhas_effect\x18\x12 \x01(\x08\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\x13 \x01(\t"2\n\x14SwitchCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"E\n\x14SubscribeLogsRequest\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x13\n\x0b\x64ump_config\x18\x02 \x01(\x08"d\n\x15SubscribeLogsResponse\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x0b\n\x03tag\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x13\n\x0bsend_failed\x18\x04 \x01(\x08"\x1e\n\x1cSubscribeServiceCallsRequest"\xdf\x02\n\x13ServiceCallResponse\x12\x0f\n\x07service\x18\x01 \x01(\t\x12,\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x1e.ServiceCallResponse.DataEntry\x12=\n\rdata_template\x18\x03 \x03(\x0b\x32&.ServiceCallResponse.DataTemplateEntry\x12\x36\n\tvariables\x18\x04 \x03(\x0b\x32#.ServiceCallResponse.VariablesEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x44\x61taTemplateEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x30\n\x0eVariablesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"%\n#SubscribeHomeAssistantStatesRequest"8\n#SubscribeHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t">\n\x1aHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t"\x10\n\x0eGetTimeRequest"(\n\x0fGetTimeResponse\x12\x15\n\repoch_seconds\x18\x01 \x01(\x07*)\n\x08\x46\x61nSpeed\x12\x07\n\x03LOW\x10\x00\x12\n\n\x06MEDIUM\x10\x01\x12\x08\n\x04HIGH\x10\x02*]\n\x08LogLevel\x12\x08\n\x04NONE\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x08\n\x04WARN\x10\x02\x12\x08\n\x04INFO\x10\x03\x12\t\n\x05\x44\x45\x42UG\x10\x04\x12\x0b\n\x07VERBOSE\x10\x05\x12\x10\n\x0cVERY_VERBOSE\x10\x06\x62\x06proto3' - ), -) - -_FANSPEED = _descriptor.EnumDescriptor( - name="FanSpeed", - full_name="FanSpeed", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="LOW", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="MEDIUM", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="HIGH", index=2, number=2, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=3822, - serialized_end=3863, -) -_sym_db.RegisterEnumDescriptor(_FANSPEED) - -FanSpeed = enum_type_wrapper.EnumTypeWrapper(_FANSPEED) -_LOGLEVEL = _descriptor.EnumDescriptor( - name="LogLevel", - full_name="LogLevel", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="NONE", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="ERROR", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="WARN", index=2, number=2, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="INFO", index=3, number=3, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="DEBUG", index=4, number=4, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="VERBOSE", index=5, number=5, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="VERY_VERBOSE", index=6, number=6, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=3865, - serialized_end=3958, -) -_sym_db.RegisterEnumDescriptor(_LOGLEVEL) - -LogLevel = enum_type_wrapper.EnumTypeWrapper(_LOGLEVEL) -LOW = 0 -MEDIUM = 1 -HIGH = 2 -NONE = 0 -ERROR = 1 -WARN = 2 -INFO = 3 -DEBUG = 4 -VERBOSE = 5 -VERY_VERBOSE = 6 - - -_COVERSTATERESPONSE_COVERSTATE = _descriptor.EnumDescriptor( - name="CoverState", - full_name="CoverStateResponse.CoverState", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="OPEN", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="CLOSED", index=1, number=1, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=1808, - serialized_end=1842, -) -_sym_db.RegisterEnumDescriptor(_COVERSTATERESPONSE_COVERSTATE) - -_COVERCOMMANDREQUEST_COVERCOMMAND = _descriptor.EnumDescriptor( - name="CoverCommand", - full_name="CoverCommandRequest.CoverCommand", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="OPEN", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="CLOSE", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="STOP", index=2, number=2, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=2375, - serialized_end=2420, -) -_sym_db.RegisterEnumDescriptor(_COVERCOMMANDREQUEST_COVERCOMMAND) - - -_HELLOREQUEST = _descriptor.Descriptor( - name="HelloRequest", - full_name="HelloRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="client_info", - full_name="HelloRequest.client_info", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=13, - serialized_end=48, -) - - -_HELLORESPONSE = _descriptor.Descriptor( - name="HelloResponse", - full_name="HelloResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="api_version_major", - full_name="HelloResponse.api_version_major", - index=0, - number=1, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="api_version_minor", - full_name="HelloResponse.api_version_minor", - index=1, - number=2, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="server_info", - full_name="HelloResponse.server_info", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=50, - serialized_end=140, -) - - -_CONNECTREQUEST = _descriptor.Descriptor( - name="ConnectRequest", - full_name="ConnectRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="password", - full_name="ConnectRequest.password", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=142, - serialized_end=176, -) - - -_CONNECTRESPONSE = _descriptor.Descriptor( - name="ConnectResponse", - full_name="ConnectResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="invalid_password", - full_name="ConnectResponse.invalid_password", - index=0, - number=1, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=178, - serialized_end=221, -) - - -_DISCONNECTREQUEST = _descriptor.Descriptor( - name="DisconnectRequest", - full_name="DisconnectRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=223, - serialized_end=242, -) - - -_DISCONNECTRESPONSE = _descriptor.Descriptor( - name="DisconnectResponse", - full_name="DisconnectResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=244, - serialized_end=264, -) - - -_PINGREQUEST = _descriptor.Descriptor( - name="PingRequest", - full_name="PingRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=266, - serialized_end=279, -) - - -_PINGRESPONSE = _descriptor.Descriptor( - name="PingResponse", - full_name="PingResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=281, - serialized_end=295, -) - - -_DEVICEINFOREQUEST = _descriptor.Descriptor( - name="DeviceInfoRequest", - full_name="DeviceInfoRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=297, - serialized_end=316, -) - - -_DEVICEINFORESPONSE = _descriptor.Descriptor( - name="DeviceInfoResponse", - full_name="DeviceInfoResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="uses_password", - full_name="DeviceInfoResponse.uses_password", - index=0, - number=1, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="DeviceInfoResponse.name", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="mac_address", - full_name="DeviceInfoResponse.mac_address", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="esphome_core_version", - full_name="DeviceInfoResponse.esphome_core_version", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="compilation_time", - full_name="DeviceInfoResponse.compilation_time", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="model", - full_name="DeviceInfoResponse.model", - index=5, - number=6, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_deep_sleep", - full_name="DeviceInfoResponse.has_deep_sleep", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=319, - serialized_end=492, -) - - -_LISTENTITIESREQUEST = _descriptor.Descriptor( - name="ListEntitiesRequest", - full_name="ListEntitiesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=494, - serialized_end=515, -) - - -_LISTENTITIESBINARYSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesBinarySensorResponse", - full_name="ListEntitiesBinarySensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesBinarySensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesBinarySensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesBinarySensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesBinarySensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="device_class", - full_name="ListEntitiesBinarySensorResponse.device_class", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="is_status_binary_sensor", - full_name="ListEntitiesBinarySensorResponse.is_status_binary_sensor", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=518, - serialized_end=672, -) - - -_LISTENTITIESCOVERRESPONSE = _descriptor.Descriptor( - name="ListEntitiesCoverResponse", - full_name="ListEntitiesCoverResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesCoverResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesCoverResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesCoverResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesCoverResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="is_optimistic", - full_name="ListEntitiesCoverResponse.is_optimistic", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=674, - serialized_end=789, -) - - -_LISTENTITIESFANRESPONSE = _descriptor.Descriptor( - name="ListEntitiesFanResponse", - full_name="ListEntitiesFanResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesFanResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesFanResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesFanResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesFanResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_oscillation", - full_name="ListEntitiesFanResponse.supports_oscillation", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_speed", - full_name="ListEntitiesFanResponse.supports_speed", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=792, - serialized_end=936, -) - - -_LISTENTITIESLIGHTRESPONSE = _descriptor.Descriptor( - name="ListEntitiesLightResponse", - full_name="ListEntitiesLightResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesLightResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesLightResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesLightResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesLightResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_brightness", - full_name="ListEntitiesLightResponse.supports_brightness", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_rgb", - full_name="ListEntitiesLightResponse.supports_rgb", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_white_value", - full_name="ListEntitiesLightResponse.supports_white_value", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_color_temperature", - full_name="ListEntitiesLightResponse.supports_color_temperature", - index=7, - number=8, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="min_mireds", - full_name="ListEntitiesLightResponse.min_mireds", - index=8, - number=9, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="max_mireds", - full_name="ListEntitiesLightResponse.max_mireds", - index=9, - number=10, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effects", - full_name="ListEntitiesLightResponse.effects", - index=10, - number=11, - type=9, - cpp_type=9, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=939, - serialized_end=1205, -) - - -_LISTENTITIESSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesSensorResponse", - full_name="ListEntitiesSensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesSensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesSensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesSensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesSensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesSensorResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unit_of_measurement", - full_name="ListEntitiesSensorResponse.unit_of_measurement", - index=5, - number=6, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="accuracy_decimals", - full_name="ListEntitiesSensorResponse.accuracy_decimals", - index=6, - number=7, - type=5, - cpp_type=1, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1208, - serialized_end=1371, -) - - -_LISTENTITIESSWITCHRESPONSE = _descriptor.Descriptor( - name="ListEntitiesSwitchResponse", - full_name="ListEntitiesSwitchResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesSwitchResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesSwitchResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesSwitchResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesSwitchResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesSwitchResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="optimistic", - full_name="ListEntitiesSwitchResponse.optimistic", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1373, - serialized_end=1500, -) - - -_LISTENTITIESTEXTSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesTextSensorResponse", - full_name="ListEntitiesTextSensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesTextSensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesTextSensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesTextSensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesTextSensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesTextSensorResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1502, - serialized_end=1613, -) - - -_LISTENTITIESDONERESPONSE = _descriptor.Descriptor( - name="ListEntitiesDoneResponse", - full_name="ListEntitiesDoneResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1615, - serialized_end=1641, -) - - -_SUBSCRIBESTATESREQUEST = _descriptor.Descriptor( - name="SubscribeStatesRequest", - full_name="SubscribeStatesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1643, - serialized_end=1667, -) - - -_BINARYSENSORSTATERESPONSE = _descriptor.Descriptor( - name="BinarySensorStateResponse", - full_name="BinarySensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="BinarySensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="BinarySensorStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1669, - serialized_end=1724, -) - - -_COVERSTATERESPONSE = _descriptor.Descriptor( - name="CoverStateResponse", - full_name="CoverStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="CoverStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="CoverStateResponse.state", - index=1, - number=2, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _COVERSTATERESPONSE_COVERSTATE, - ], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1726, - serialized_end=1842, -) - - -_FANSTATERESPONSE = _descriptor.Descriptor( - name="FanStateResponse", - full_name="FanStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="FanStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="FanStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="oscillating", - full_name="FanStateResponse.oscillating", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="speed", - full_name="FanStateResponse.speed", - index=3, - number=4, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1844, - serialized_end=1937, -) - - -_LIGHTSTATERESPONSE = _descriptor.Descriptor( - name="LightStateResponse", - full_name="LightStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="LightStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="LightStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="brightness", - full_name="LightStateResponse.brightness", - index=2, - number=3, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="red", - full_name="LightStateResponse.red", - index=3, - number=4, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="green", - full_name="LightStateResponse.green", - index=4, - number=5, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="blue", - full_name="LightStateResponse.blue", - index=5, - number=6, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="white", - full_name="LightStateResponse.white", - index=6, - number=7, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="color_temperature", - full_name="LightStateResponse.color_temperature", - index=7, - number=8, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effect", - full_name="LightStateResponse.effect", - index=8, - number=9, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1940, - serialized_end=2108, -) - - -_SENSORSTATERESPONSE = _descriptor.Descriptor( - name="SensorStateResponse", - full_name="SensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SensorStateResponse.state", - index=1, - number=2, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2110, - serialized_end=2159, -) - - -_SWITCHSTATERESPONSE = _descriptor.Descriptor( - name="SwitchStateResponse", - full_name="SwitchStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SwitchStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SwitchStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2161, - serialized_end=2210, -) - - -_TEXTSENSORSTATERESPONSE = _descriptor.Descriptor( - name="TextSensorStateResponse", - full_name="TextSensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="TextSensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="TextSensorStateResponse.state", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2212, - serialized_end=2265, -) - - -_COVERCOMMANDREQUEST = _descriptor.Descriptor( - name="CoverCommandRequest", - full_name="CoverCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="CoverCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="CoverCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="command", - full_name="CoverCommandRequest.command", - index=2, - number=3, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _COVERCOMMANDREQUEST_COVERCOMMAND, - ], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2268, - serialized_end=2420, -) - - -_FANCOMMANDREQUEST = _descriptor.Descriptor( - name="FanCommandRequest", - full_name="FanCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="FanCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="FanCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="FanCommandRequest.state", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_speed", - full_name="FanCommandRequest.has_speed", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="speed", - full_name="FanCommandRequest.speed", - index=4, - number=5, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_oscillating", - full_name="FanCommandRequest.has_oscillating", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="oscillating", - full_name="FanCommandRequest.oscillating", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2423, - serialized_end=2580, -) - - -_LIGHTCOMMANDREQUEST = _descriptor.Descriptor( - name="LightCommandRequest", - full_name="LightCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="LightCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="LightCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="LightCommandRequest.state", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_brightness", - full_name="LightCommandRequest.has_brightness", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="brightness", - full_name="LightCommandRequest.brightness", - index=4, - number=5, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_rgb", - full_name="LightCommandRequest.has_rgb", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="red", - full_name="LightCommandRequest.red", - index=6, - number=7, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="green", - full_name="LightCommandRequest.green", - index=7, - number=8, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="blue", - full_name="LightCommandRequest.blue", - index=8, - number=9, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_white", - full_name="LightCommandRequest.has_white", - index=9, - number=10, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="white", - full_name="LightCommandRequest.white", - index=10, - number=11, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_color_temperature", - full_name="LightCommandRequest.has_color_temperature", - index=11, - number=12, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="color_temperature", - full_name="LightCommandRequest.color_temperature", - index=12, - number=13, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_transition_length", - full_name="LightCommandRequest.has_transition_length", - index=13, - number=14, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="transition_length", - full_name="LightCommandRequest.transition_length", - index=14, - number=15, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_flash_length", - full_name="LightCommandRequest.has_flash_length", - index=15, - number=16, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="flash_length", - full_name="LightCommandRequest.flash_length", - index=16, - number=17, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_effect", - full_name="LightCommandRequest.has_effect", - index=17, - number=18, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effect", - full_name="LightCommandRequest.effect", - index=18, - number=19, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2583, - serialized_end=2988, -) - - -_SWITCHCOMMANDREQUEST = _descriptor.Descriptor( - name="SwitchCommandRequest", - full_name="SwitchCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SwitchCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SwitchCommandRequest.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2990, - serialized_end=3040, -) - - -_SUBSCRIBELOGSREQUEST = _descriptor.Descriptor( - name="SubscribeLogsRequest", - full_name="SubscribeLogsRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="level", - full_name="SubscribeLogsRequest.level", - index=0, - number=1, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="dump_config", - full_name="SubscribeLogsRequest.dump_config", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3042, - serialized_end=3111, -) - - -_SUBSCRIBELOGSRESPONSE = _descriptor.Descriptor( - name="SubscribeLogsResponse", - full_name="SubscribeLogsResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="level", - full_name="SubscribeLogsResponse.level", - index=0, - number=1, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="tag", - full_name="SubscribeLogsResponse.tag", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="message", - full_name="SubscribeLogsResponse.message", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="send_failed", - full_name="SubscribeLogsResponse.send_failed", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3113, - serialized_end=3213, -) - - -_SUBSCRIBESERVICECALLSREQUEST = _descriptor.Descriptor( - name="SubscribeServiceCallsRequest", - full_name="SubscribeServiceCallsRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3215, - serialized_end=3245, -) - - -_SERVICECALLRESPONSE_DATAENTRY = _descriptor.Descriptor( - name="DataEntry", - full_name="ServiceCallResponse.DataEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.DataEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.DataEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3453, - serialized_end=3496, -) - -_SERVICECALLRESPONSE_DATATEMPLATEENTRY = _descriptor.Descriptor( - name="DataTemplateEntry", - full_name="ServiceCallResponse.DataTemplateEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.DataTemplateEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.DataTemplateEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3498, - serialized_end=3549, -) - -_SERVICECALLRESPONSE_VARIABLESENTRY = _descriptor.Descriptor( - name="VariablesEntry", - full_name="ServiceCallResponse.VariablesEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.VariablesEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.VariablesEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3551, - serialized_end=3599, -) - -_SERVICECALLRESPONSE = _descriptor.Descriptor( - name="ServiceCallResponse", - full_name="ServiceCallResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="service", - full_name="ServiceCallResponse.service", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="data", - full_name="ServiceCallResponse.data", - index=1, - number=2, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="data_template", - full_name="ServiceCallResponse.data_template", - index=2, - number=3, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="variables", - full_name="ServiceCallResponse.variables", - index=3, - number=4, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[ - _SERVICECALLRESPONSE_DATAENTRY, - _SERVICECALLRESPONSE_DATATEMPLATEENTRY, - _SERVICECALLRESPONSE_VARIABLESENTRY, - ], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3248, - serialized_end=3599, -) - - -_SUBSCRIBEHOMEASSISTANTSTATESREQUEST = _descriptor.Descriptor( - name="SubscribeHomeAssistantStatesRequest", - full_name="SubscribeHomeAssistantStatesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3601, - serialized_end=3638, -) - - -_SUBSCRIBEHOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor( - name="SubscribeHomeAssistantStateResponse", - full_name="SubscribeHomeAssistantStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="entity_id", - full_name="SubscribeHomeAssistantStateResponse.entity_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3640, - serialized_end=3696, -) - - -_HOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor( - name="HomeAssistantStateResponse", - full_name="HomeAssistantStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="entity_id", - full_name="HomeAssistantStateResponse.entity_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="HomeAssistantStateResponse.state", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3698, - serialized_end=3760, -) - - -_GETTIMEREQUEST = _descriptor.Descriptor( - name="GetTimeRequest", - full_name="GetTimeRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3762, - serialized_end=3778, -) - - -_GETTIMERESPONSE = _descriptor.Descriptor( - name="GetTimeResponse", - full_name="GetTimeResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="epoch_seconds", - full_name="GetTimeResponse.epoch_seconds", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3780, - serialized_end=3820, -) - -_COVERSTATERESPONSE.fields_by_name["state"].enum_type = _COVERSTATERESPONSE_COVERSTATE -_COVERSTATERESPONSE_COVERSTATE.containing_type = _COVERSTATERESPONSE -_FANSTATERESPONSE.fields_by_name["speed"].enum_type = _FANSPEED -_COVERCOMMANDREQUEST.fields_by_name[ - "command" -].enum_type = _COVERCOMMANDREQUEST_COVERCOMMAND -_COVERCOMMANDREQUEST_COVERCOMMAND.containing_type = _COVERCOMMANDREQUEST -_FANCOMMANDREQUEST.fields_by_name["speed"].enum_type = _FANSPEED -_SUBSCRIBELOGSREQUEST.fields_by_name["level"].enum_type = _LOGLEVEL -_SUBSCRIBELOGSRESPONSE.fields_by_name["level"].enum_type = _LOGLEVEL -_SERVICECALLRESPONSE_DATAENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE_DATATEMPLATEENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE_VARIABLESENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE.fields_by_name[ - "data" -].message_type = _SERVICECALLRESPONSE_DATAENTRY -_SERVICECALLRESPONSE.fields_by_name[ - "data_template" -].message_type = _SERVICECALLRESPONSE_DATATEMPLATEENTRY -_SERVICECALLRESPONSE.fields_by_name[ - "variables" -].message_type = _SERVICECALLRESPONSE_VARIABLESENTRY -DESCRIPTOR.message_types_by_name["HelloRequest"] = _HELLOREQUEST -DESCRIPTOR.message_types_by_name["HelloResponse"] = _HELLORESPONSE -DESCRIPTOR.message_types_by_name["ConnectRequest"] = _CONNECTREQUEST -DESCRIPTOR.message_types_by_name["ConnectResponse"] = _CONNECTRESPONSE -DESCRIPTOR.message_types_by_name["DisconnectRequest"] = _DISCONNECTREQUEST -DESCRIPTOR.message_types_by_name["DisconnectResponse"] = _DISCONNECTRESPONSE -DESCRIPTOR.message_types_by_name["PingRequest"] = _PINGREQUEST -DESCRIPTOR.message_types_by_name["PingResponse"] = _PINGRESPONSE -DESCRIPTOR.message_types_by_name["DeviceInfoRequest"] = _DEVICEINFOREQUEST -DESCRIPTOR.message_types_by_name["DeviceInfoResponse"] = _DEVICEINFORESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesRequest"] = _LISTENTITIESREQUEST -DESCRIPTOR.message_types_by_name[ - "ListEntitiesBinarySensorResponse" -] = _LISTENTITIESBINARYSENSORRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesCoverResponse" -] = _LISTENTITIESCOVERRESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesFanResponse"] = _LISTENTITIESFANRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesLightResponse" -] = _LISTENTITIESLIGHTRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesSensorResponse" -] = _LISTENTITIESSENSORRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesSwitchResponse" -] = _LISTENTITIESSWITCHRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesTextSensorResponse" -] = _LISTENTITIESTEXTSENSORRESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesDoneResponse"] = _LISTENTITIESDONERESPONSE -DESCRIPTOR.message_types_by_name["SubscribeStatesRequest"] = _SUBSCRIBESTATESREQUEST -DESCRIPTOR.message_types_by_name[ - "BinarySensorStateResponse" -] = _BINARYSENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["CoverStateResponse"] = _COVERSTATERESPONSE -DESCRIPTOR.message_types_by_name["FanStateResponse"] = _FANSTATERESPONSE -DESCRIPTOR.message_types_by_name["LightStateResponse"] = _LIGHTSTATERESPONSE -DESCRIPTOR.message_types_by_name["SensorStateResponse"] = _SENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["SwitchStateResponse"] = _SWITCHSTATERESPONSE -DESCRIPTOR.message_types_by_name["TextSensorStateResponse"] = _TEXTSENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["CoverCommandRequest"] = _COVERCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["FanCommandRequest"] = _FANCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["LightCommandRequest"] = _LIGHTCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["SwitchCommandRequest"] = _SWITCHCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["SubscribeLogsRequest"] = _SUBSCRIBELOGSREQUEST -DESCRIPTOR.message_types_by_name["SubscribeLogsResponse"] = _SUBSCRIBELOGSRESPONSE -DESCRIPTOR.message_types_by_name[ - "SubscribeServiceCallsRequest" -] = _SUBSCRIBESERVICECALLSREQUEST -DESCRIPTOR.message_types_by_name["ServiceCallResponse"] = _SERVICECALLRESPONSE -DESCRIPTOR.message_types_by_name[ - "SubscribeHomeAssistantStatesRequest" -] = _SUBSCRIBEHOMEASSISTANTSTATESREQUEST -DESCRIPTOR.message_types_by_name[ - "SubscribeHomeAssistantStateResponse" -] = _SUBSCRIBEHOMEASSISTANTSTATERESPONSE -DESCRIPTOR.message_types_by_name[ - "HomeAssistantStateResponse" -] = _HOMEASSISTANTSTATERESPONSE -DESCRIPTOR.message_types_by_name["GetTimeRequest"] = _GETTIMEREQUEST -DESCRIPTOR.message_types_by_name["GetTimeResponse"] = _GETTIMERESPONSE -DESCRIPTOR.enum_types_by_name["FanSpeed"] = _FANSPEED -DESCRIPTOR.enum_types_by_name["LogLevel"] = _LOGLEVEL -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -HelloRequest = _reflection.GeneratedProtocolMessageType( - "HelloRequest", - (_message.Message,), - dict( - DESCRIPTOR=_HELLOREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HelloRequest) - ), -) -_sym_db.RegisterMessage(HelloRequest) - -HelloResponse = _reflection.GeneratedProtocolMessageType( - "HelloResponse", - (_message.Message,), - dict( - DESCRIPTOR=_HELLORESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HelloResponse) - ), -) -_sym_db.RegisterMessage(HelloResponse) - -ConnectRequest = _reflection.GeneratedProtocolMessageType( - "ConnectRequest", - (_message.Message,), - dict( - DESCRIPTOR=_CONNECTREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ConnectRequest) - ), -) -_sym_db.RegisterMessage(ConnectRequest) - -ConnectResponse = _reflection.GeneratedProtocolMessageType( - "ConnectResponse", - (_message.Message,), - dict( - DESCRIPTOR=_CONNECTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ConnectResponse) - ), -) -_sym_db.RegisterMessage(ConnectResponse) - -DisconnectRequest = _reflection.GeneratedProtocolMessageType( - "DisconnectRequest", - (_message.Message,), - dict( - DESCRIPTOR=_DISCONNECTREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DisconnectRequest) - ), -) -_sym_db.RegisterMessage(DisconnectRequest) - -DisconnectResponse = _reflection.GeneratedProtocolMessageType( - "DisconnectResponse", - (_message.Message,), - dict( - DESCRIPTOR=_DISCONNECTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DisconnectResponse) - ), -) -_sym_db.RegisterMessage(DisconnectResponse) - -PingRequest = _reflection.GeneratedProtocolMessageType( - "PingRequest", - (_message.Message,), - dict( - DESCRIPTOR=_PINGREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:PingRequest) - ), -) -_sym_db.RegisterMessage(PingRequest) - -PingResponse = _reflection.GeneratedProtocolMessageType( - "PingResponse", - (_message.Message,), - dict( - DESCRIPTOR=_PINGRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:PingResponse) - ), -) -_sym_db.RegisterMessage(PingResponse) - -DeviceInfoRequest = _reflection.GeneratedProtocolMessageType( - "DeviceInfoRequest", - (_message.Message,), - dict( - DESCRIPTOR=_DEVICEINFOREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DeviceInfoRequest) - ), -) -_sym_db.RegisterMessage(DeviceInfoRequest) - -DeviceInfoResponse = _reflection.GeneratedProtocolMessageType( - "DeviceInfoResponse", - (_message.Message,), - dict( - DESCRIPTOR=_DEVICEINFORESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DeviceInfoResponse) - ), -) -_sym_db.RegisterMessage(DeviceInfoResponse) - -ListEntitiesRequest = _reflection.GeneratedProtocolMessageType( - "ListEntitiesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesRequest) - ), -) -_sym_db.RegisterMessage(ListEntitiesRequest) - -ListEntitiesBinarySensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesBinarySensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESBINARYSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesBinarySensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesBinarySensorResponse) - -ListEntitiesCoverResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesCoverResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESCOVERRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesCoverResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesCoverResponse) - -ListEntitiesFanResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesFanResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESFANRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesFanResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesFanResponse) - -ListEntitiesLightResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesLightResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESLIGHTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesLightResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesLightResponse) - -ListEntitiesSensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesSensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesSensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesSensorResponse) - -ListEntitiesSwitchResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesSwitchResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESSWITCHRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesSwitchResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesSwitchResponse) - -ListEntitiesTextSensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesTextSensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESTEXTSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesTextSensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesTextSensorResponse) - -ListEntitiesDoneResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesDoneResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESDONERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesDoneResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesDoneResponse) - -SubscribeStatesRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeStatesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBESTATESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeStatesRequest) - ), -) -_sym_db.RegisterMessage(SubscribeStatesRequest) - -BinarySensorStateResponse = _reflection.GeneratedProtocolMessageType( - "BinarySensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_BINARYSENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:BinarySensorStateResponse) - ), -) -_sym_db.RegisterMessage(BinarySensorStateResponse) - -CoverStateResponse = _reflection.GeneratedProtocolMessageType( - "CoverStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_COVERSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:CoverStateResponse) - ), -) -_sym_db.RegisterMessage(CoverStateResponse) - -FanStateResponse = _reflection.GeneratedProtocolMessageType( - "FanStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_FANSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:FanStateResponse) - ), -) -_sym_db.RegisterMessage(FanStateResponse) - -LightStateResponse = _reflection.GeneratedProtocolMessageType( - "LightStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LIGHTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:LightStateResponse) - ), -) -_sym_db.RegisterMessage(LightStateResponse) - -SensorStateResponse = _reflection.GeneratedProtocolMessageType( - "SensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SensorStateResponse) - ), -) -_sym_db.RegisterMessage(SensorStateResponse) - -SwitchStateResponse = _reflection.GeneratedProtocolMessageType( - "SwitchStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SWITCHSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SwitchStateResponse) - ), -) -_sym_db.RegisterMessage(SwitchStateResponse) - -TextSensorStateResponse = _reflection.GeneratedProtocolMessageType( - "TextSensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_TEXTSENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:TextSensorStateResponse) - ), -) -_sym_db.RegisterMessage(TextSensorStateResponse) - -CoverCommandRequest = _reflection.GeneratedProtocolMessageType( - "CoverCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_COVERCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:CoverCommandRequest) - ), -) -_sym_db.RegisterMessage(CoverCommandRequest) - -FanCommandRequest = _reflection.GeneratedProtocolMessageType( - "FanCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_FANCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:FanCommandRequest) - ), -) -_sym_db.RegisterMessage(FanCommandRequest) - -LightCommandRequest = _reflection.GeneratedProtocolMessageType( - "LightCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_LIGHTCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:LightCommandRequest) - ), -) -_sym_db.RegisterMessage(LightCommandRequest) - -SwitchCommandRequest = _reflection.GeneratedProtocolMessageType( - "SwitchCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SWITCHCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SwitchCommandRequest) - ), -) -_sym_db.RegisterMessage(SwitchCommandRequest) - -SubscribeLogsRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeLogsRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBELOGSREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeLogsRequest) - ), -) -_sym_db.RegisterMessage(SubscribeLogsRequest) - -SubscribeLogsResponse = _reflection.GeneratedProtocolMessageType( - "SubscribeLogsResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBELOGSRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeLogsResponse) - ), -) -_sym_db.RegisterMessage(SubscribeLogsResponse) - -SubscribeServiceCallsRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeServiceCallsRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBESERVICECALLSREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeServiceCallsRequest) - ), -) -_sym_db.RegisterMessage(SubscribeServiceCallsRequest) - -ServiceCallResponse = _reflection.GeneratedProtocolMessageType( - "ServiceCallResponse", - (_message.Message,), - dict( - DataEntry=_reflection.GeneratedProtocolMessageType( - "DataEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_DATAENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataEntry) - ), - ), - DataTemplateEntry=_reflection.GeneratedProtocolMessageType( - "DataTemplateEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_DATATEMPLATEENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataTemplateEntry) - ), - ), - VariablesEntry=_reflection.GeneratedProtocolMessageType( - "VariablesEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_VARIABLESENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.VariablesEntry) - ), - ), - DESCRIPTOR=_SERVICECALLRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse) - ), -) -_sym_db.RegisterMessage(ServiceCallResponse) -_sym_db.RegisterMessage(ServiceCallResponse.DataEntry) -_sym_db.RegisterMessage(ServiceCallResponse.DataTemplateEntry) -_sym_db.RegisterMessage(ServiceCallResponse.VariablesEntry) - -SubscribeHomeAssistantStatesRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeHomeAssistantStatesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStatesRequest) - ), -) -_sym_db.RegisterMessage(SubscribeHomeAssistantStatesRequest) - -SubscribeHomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType( - "SubscribeHomeAssistantStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStateResponse) - ), -) -_sym_db.RegisterMessage(SubscribeHomeAssistantStateResponse) - -HomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType( - "HomeAssistantStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_HOMEASSISTANTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HomeAssistantStateResponse) - ), -) -_sym_db.RegisterMessage(HomeAssistantStateResponse) - -GetTimeRequest = _reflection.GeneratedProtocolMessageType( - "GetTimeRequest", - (_message.Message,), - dict( - DESCRIPTOR=_GETTIMEREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:GetTimeRequest) - ), -) -_sym_db.RegisterMessage(GetTimeRequest) - -GetTimeResponse = _reflection.GeneratedProtocolMessageType( - "GetTimeResponse", - (_message.Message,), - dict( - DESCRIPTOR=_GETTIMERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:GetTimeResponse) - ), -) -_sym_db.RegisterMessage(GetTimeResponse) - - -_SERVICECALLRESPONSE_DATAENTRY._options = None -_SERVICECALLRESPONSE_DATATEMPLATEENTRY._options = None -_SERVICECALLRESPONSE_VARIABLESENTRY._options = None -# @@protoc_insertion_point(module_scope) diff --git a/esphome/api/client.py b/esphome/api/client.py deleted file mode 100644 index dd11f79922..0000000000 --- a/esphome/api/client.py +++ /dev/null @@ -1,518 +0,0 @@ -from datetime import datetime -import functools -import logging -import socket -import threading -import time - -# pylint: disable=unused-import -from typing import Optional # noqa -from google.protobuf import message # noqa - -from esphome import const -import esphome.api.api_pb2 as pb -from esphome.const import CONF_PASSWORD, CONF_PORT -from esphome.core import EsphomeError -from esphome.helpers import resolve_ip_address, indent -from esphome.log import color, Fore -from esphome.util import safe_print - -_LOGGER = logging.getLogger(__name__) - - -class APIConnectionError(EsphomeError): - pass - - -MESSAGE_TYPE_TO_PROTO = { - 1: pb.HelloRequest, - 2: pb.HelloResponse, - 3: pb.ConnectRequest, - 4: pb.ConnectResponse, - 5: pb.DisconnectRequest, - 6: pb.DisconnectResponse, - 7: pb.PingRequest, - 8: pb.PingResponse, - 9: pb.DeviceInfoRequest, - 10: pb.DeviceInfoResponse, - 11: pb.ListEntitiesRequest, - 12: pb.ListEntitiesBinarySensorResponse, - 13: pb.ListEntitiesCoverResponse, - 14: pb.ListEntitiesFanResponse, - 15: pb.ListEntitiesLightResponse, - 16: pb.ListEntitiesSensorResponse, - 17: pb.ListEntitiesSwitchResponse, - 18: pb.ListEntitiesTextSensorResponse, - 19: pb.ListEntitiesDoneResponse, - 20: pb.SubscribeStatesRequest, - 21: pb.BinarySensorStateResponse, - 22: pb.CoverStateResponse, - 23: pb.FanStateResponse, - 24: pb.LightStateResponse, - 25: pb.SensorStateResponse, - 26: pb.SwitchStateResponse, - 27: pb.TextSensorStateResponse, - 28: pb.SubscribeLogsRequest, - 29: pb.SubscribeLogsResponse, - 30: pb.CoverCommandRequest, - 31: pb.FanCommandRequest, - 32: pb.LightCommandRequest, - 33: pb.SwitchCommandRequest, - 34: pb.SubscribeServiceCallsRequest, - 35: pb.ServiceCallResponse, - 36: pb.GetTimeRequest, - 37: pb.GetTimeResponse, -} - - -def _varuint_to_bytes(value): - if value <= 0x7F: - return bytes([value]) - - ret = bytes() - while value: - temp = value & 0x7F - value >>= 7 - if value: - ret += bytes([temp | 0x80]) - else: - ret += bytes([temp]) - - return ret - - -def _bytes_to_varuint(value): - result = 0 - bitpos = 0 - for val in value: - result |= (val & 0x7F) << bitpos - bitpos += 7 - if (val & 0x80) == 0: - return result - return None - - -# pylint: disable=too-many-instance-attributes,not-callable -class APIClient(threading.Thread): - def __init__(self, address, port, password): - threading.Thread.__init__(self) - self._address = address # type: str - self._port = port # type: int - self._password = password # type: Optional[str] - self._socket = None # type: Optional[socket.socket] - self._socket_open_event = threading.Event() - self._socket_write_lock = threading.Lock() - self._connected = False - self._authenticated = False - self._message_handlers = [] - self._keepalive = 5 - self._ping_timer = None - - self.on_disconnect = None - self.on_connect = None - self.on_login = None - self.auto_reconnect = False - self._running_event = threading.Event() - self._stop_event = threading.Event() - - @property - def stopped(self): - return self._stop_event.is_set() - - def _refresh_ping(self): - if self._ping_timer is not None: - self._ping_timer.cancel() - self._ping_timer = None - - def func(): - self._ping_timer = None - - if self._connected: - try: - self.ping() - except APIConnectionError as err: - self._fatal_error(err) - else: - self._refresh_ping() - - self._ping_timer = threading.Timer(self._keepalive, func) - self._ping_timer.start() - - def _cancel_ping(self): - if self._ping_timer is not None: - self._ping_timer.cancel() - self._ping_timer = None - - def _close_socket(self): - self._cancel_ping() - if self._socket is not None: - self._socket.close() - self._socket = None - self._socket_open_event.clear() - self._connected = False - self._authenticated = False - self._message_handlers = [] - - def stop(self, force=False): - if self.stopped: - raise ValueError - - if self._connected and not force: - try: - self.disconnect() - except APIConnectionError: - pass - self._close_socket() - - self._stop_event.set() - if not force: - self.join() - - def connect(self): - if not self._running_event.wait(0.1): - raise APIConnectionError("You need to call start() first!") - - if self._connected: - self.disconnect(on_disconnect=False) - - try: - ip = resolve_ip_address(self._address) - except EsphomeError as err: - _LOGGER.warning( - "Error resolving IP address of %s. Is it connected to WiFi?", - self._address, - ) - _LOGGER.warning( - "(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)" - ) - raise APIConnectionError(err) from err - - _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip) - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.settimeout(10.0) - self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - try: - self._socket.connect((ip, self._port)) - except OSError as err: - err = APIConnectionError(f"Error connecting to {ip}: {err}") - self._fatal_error(err) - raise err - self._socket.settimeout(0.1) - - self._socket_open_event.set() - - hello = pb.HelloRequest() - hello.client_info = f"ESPHome v{const.__version__}" - try: - resp = self._send_message_await_response(hello, pb.HelloResponse) - except APIConnectionError as err: - self._fatal_error(err) - raise err - _LOGGER.debug( - "Successfully connected to %s ('%s' API=%s.%s)", - self._address, - resp.server_info, - resp.api_version_major, - resp.api_version_minor, - ) - self._connected = True - self._refresh_ping() - if self.on_connect is not None: - self.on_connect() - - def _check_connected(self): - if not self._connected: - err = APIConnectionError("Must be connected!") - self._fatal_error(err) - raise err - - def login(self): - self._check_connected() - if self._authenticated: - raise APIConnectionError("Already logged in!") - - connect = pb.ConnectRequest() - if self._password is not None: - connect.password = self._password - resp = self._send_message_await_response(connect, pb.ConnectResponse) - if resp.invalid_password: - raise APIConnectionError("Invalid password!") - - self._authenticated = True - if self.on_login is not None: - self.on_login() - - def _fatal_error(self, err): - was_connected = self._connected - - self._close_socket() - - if was_connected and self.on_disconnect is not None: - self.on_disconnect(err) - - def _write(self, data): # type: (bytes) -> None - if self._socket is None: - raise APIConnectionError("Socket closed") - - # _LOGGER.debug("Write: %s", format_bytes(data)) - with self._socket_write_lock: - try: - self._socket.sendall(data) - except OSError as err: - err = APIConnectionError(f"Error while writing data: {err}") - self._fatal_error(err) - raise err - - def _send_message(self, msg): - # type: (message.Message) -> None - for message_type, klass in MESSAGE_TYPE_TO_PROTO.items(): - if isinstance(msg, klass): - break - else: - raise ValueError - - encoded = msg.SerializeToString() - _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) - req = bytes([0]) - req += _varuint_to_bytes(len(encoded)) - req += _varuint_to_bytes(message_type) - req += encoded - self._write(req) - - def _send_message_await_response_complex( - self, send_msg, do_append, do_stop, timeout=5 - ): - event = threading.Event() - responses = [] - - def on_message(resp): - if do_append(resp): - responses.append(resp) - if do_stop(resp): - event.set() - - self._message_handlers.append(on_message) - self._send_message(send_msg) - ret = event.wait(timeout) - try: - self._message_handlers.remove(on_message) - except ValueError: - pass - if not ret: - raise APIConnectionError("Timeout while waiting for message response!") - return responses - - def _send_message_await_response(self, send_msg, response_type, timeout=5): - def is_response(msg): - return isinstance(msg, response_type) - - return self._send_message_await_response_complex( - send_msg, is_response, is_response, timeout - )[0] - - def device_info(self): - self._check_connected() - return self._send_message_await_response( - pb.DeviceInfoRequest(), pb.DeviceInfoResponse - ) - - def ping(self): - self._check_connected() - return self._send_message_await_response(pb.PingRequest(), pb.PingResponse) - - def disconnect(self, on_disconnect=True): - self._check_connected() - - try: - self._send_message_await_response( - pb.DisconnectRequest(), pb.DisconnectResponse - ) - except APIConnectionError: - pass - self._close_socket() - - if self.on_disconnect is not None and on_disconnect: - self.on_disconnect(None) - - def _check_authenticated(self): - if not self._authenticated: - raise APIConnectionError("Must login first!") - - def subscribe_logs(self, on_log, log_level=7, dump_config=False): - self._check_authenticated() - - def on_msg(msg): - if isinstance(msg, pb.SubscribeLogsResponse): - on_log(msg) - - self._message_handlers.append(on_msg) - req = pb.SubscribeLogsRequest(dump_config=dump_config) - req.level = log_level - self._send_message(req) - - def _recv(self, amount): - ret = bytes() - if amount == 0: - return ret - - while len(ret) < amount: - if self.stopped: - raise APIConnectionError("Stopped!") - if not self._socket_open_event.is_set(): - raise APIConnectionError("No socket!") - try: - val = self._socket.recv(amount - len(ret)) - except AttributeError as err: - raise APIConnectionError("Socket was closed") from err - except socket.timeout: - continue - except OSError as err: - raise APIConnectionError(f"Error while receiving data: {err}") from err - ret += val - return ret - - def _recv_varint(self): - raw = bytes() - while not raw or raw[-1] & 0x80: - raw += self._recv(1) - return _bytes_to_varuint(raw) - - def _run_once(self): - if not self._socket_open_event.wait(0.1): - return - - # Preamble - if self._recv(1)[0] != 0x00: - raise APIConnectionError("Invalid preamble") - - length = self._recv_varint() - msg_type = self._recv_varint() - - raw_msg = self._recv(length) - if msg_type not in MESSAGE_TYPE_TO_PROTO: - _LOGGER.debug("Skipping message type %s", msg_type) - return - - msg = MESSAGE_TYPE_TO_PROTO[msg_type]() - msg.ParseFromString(raw_msg) - _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg))) - for msg_handler in self._message_handlers[:]: - msg_handler(msg) - self._handle_internal_messages(msg) - - def run(self): - self._running_event.set() - while not self.stopped: - try: - self._run_once() - except APIConnectionError as err: - if self.stopped: - break - if self._connected: - _LOGGER.error("Error while reading incoming messages: %s", err) - self._fatal_error(err) - self._running_event.clear() - - def _handle_internal_messages(self, msg): - if isinstance(msg, pb.DisconnectRequest): - self._send_message(pb.DisconnectResponse()) - if self._socket is not None: - self._socket.close() - self._socket = None - self._connected = False - if self.on_disconnect is not None: - self.on_disconnect(None) - elif isinstance(msg, pb.PingRequest): - self._send_message(pb.PingResponse()) - elif isinstance(msg, pb.GetTimeRequest): - resp = pb.GetTimeResponse() - resp.epoch_seconds = int(time.time()) - self._send_message(resp) - - -def run_logs(config, address): - conf = config["api"] - port = conf[CONF_PORT] - password = conf[CONF_PASSWORD] - _LOGGER.info("Starting log output from %s using esphome API", address) - - cli = APIClient(address, port, password) - stopping = False - retry_timer = [] - - has_connects = [] - - def try_connect(err, tries=0): - if stopping: - return - - if err: - _LOGGER.warning("Disconnected from API: %s", err) - - while retry_timer: - retry_timer.pop(0).cancel() - - error = None - try: - cli.connect() - cli.login() - except APIConnectionError as err2: # noqa - error = err2 - - if error is None: - _LOGGER.info("Successfully connected to %s", address) - return - - wait_time = int(min(1.5 ** min(tries, 100), 30)) - if not has_connects: - _LOGGER.warning( - "Initial connection failed. The ESP might not be connected " - "to WiFi yet (%s). Re-Trying in %s seconds", - error, - wait_time, - ) - else: - _LOGGER.warning( - "Couldn't connect to API (%s). Trying to reconnect in %s seconds", - error, - wait_time, - ) - timer = threading.Timer( - wait_time, functools.partial(try_connect, None, tries + 1) - ) - timer.start() - retry_timer.append(timer) - - def on_log(msg): - time_ = datetime.now().time().strftime("[%H:%M:%S]") - text = msg.message - if msg.send_failed: - text = color( - Fore.WHITE, - "(Message skipped because it was too big to fit in " - "TCP buffer - This is only cosmetic)", - ) - safe_print(time_ + text) - - def on_login(): - try: - cli.subscribe_logs(on_log, dump_config=not has_connects) - has_connects.append(True) - except APIConnectionError: - cli.disconnect() - - cli.on_disconnect = try_connect - cli.on_login = on_login - cli.start() - - try: - try_connect(None) - while True: - time.sleep(1) - except KeyboardInterrupt: - stopping = True - cli.stop(True) - while retry_timer: - retry_timer.pop(0).cancel() - return 0 diff --git a/esphome/automation.py b/esphome/automation.py index 71c564b906..0768bf8869 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_ELSE, CONF_ID, CONF_THEN, + CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TYPE_ID, CONF_TIME, @@ -244,6 +245,9 @@ def validate_wait_until(value): schema = cv.Schema( { cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Optional(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), } ) if isinstance(value, dict) and CONF_CONDITION in value: @@ -255,6 +259,9 @@ def validate_wait_until(value): async def wait_until_action_to_code(config, action_id, template_arg, args): conditions = await build_condition(config[CONF_CONDITION], template_arg, args) var = cg.new_Pvariable(action_id, template_arg, conditions) + if CONF_TIMEOUT in config: + template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32) + cg.add(var.set_timeout_value(template_)) await cg.register_component(var, {}) return var diff --git a/esphome/codegen.py b/esphome/codegen.py index 8361faeb81..4f9f67245d 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -30,6 +30,7 @@ from esphome.cpp_generator import ( # noqa add_library, add_build_flag, add_define, + add_platformio_option, get_variable, get_variable_with_full_id, process_lambda, @@ -60,12 +61,13 @@ from esphome.cpp_types import ( # noqa uint8, uint16, uint32, + uint64, int32, const_char_ptr, NAN, esphome_ns, App, - Nameable, + EntityBase, Component, ComponentPtr, PollingComponent, @@ -77,4 +79,6 @@ from esphome.cpp_types import ( # noqa JsonObjectConstRef, Controller, GPIOPin, + InternalGPIOPin, + gpio_Flags, ) diff --git a/esphome/components/a4988/a4988.h b/esphome/components/a4988/a4988.h index 5be0f3ce69..0fe7891110 100644 --- a/esphome/components/a4988/a4988.h +++ b/esphome/components/a4988/a4988.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/stepper/stepper.h" namespace esphome { diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index ad6018268e..a7f1e6f3a9 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -1,10 +1,16 @@ +#ifdef USE_ARDUINO + #include "ac_dimmer.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #endif +#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include +#endif namespace esphome { namespace ac_dimmer { @@ -17,12 +23,15 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no /// Time in microseconds the gate should be held high /// 10µs should be long enough for most triacs /// For reference: BT136 datasheet says 2µs nominal (page 7) -static const uint32_t GATE_ENABLE_TIME = 10; +/// However other factors like gate driver propagation time +/// are also considered and a really low value is not important +/// See also: https://github.com/esphome/issues/issues/1632 +static const uint32_t GATE_ENABLE_TIME = 50; /// Function called from timer interrupt /// Input is current time in microseconds (micros()) /// Returns when next "event" is expected in µs, or 0 if no such event known. -uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { +uint32_t IRAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { // If no ZC signal received yet. if (this->crossed_zero_at == 0) return 0; @@ -34,13 +43,13 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) { this->enable_time_us = 0; - this->gate_pin->digital_write(true); + this->gate_pin.digital_write(true); // Prevent too short pulses - this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); + this->disable_time_us = std::max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); } if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) { this->disable_time_us = 0; - this->gate_pin->digital_write(false); + this->gate_pin.digital_write(false); } if (time_since_zc < this->enable_time_us) @@ -60,7 +69,7 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { } /// Run timer interrupt code and return in how many µs the next event is expected -uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { +uint32_t IRAM_ATTR HOT timer_interrupt() { // run at least with 1kHz uint32_t min_dt_us = 1000; uint32_t now = micros(); @@ -77,7 +86,7 @@ uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { } /// GPIO interrupt routine, called when ZC pin triggers -void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { +void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { uint32_t prev_crossed = this->crossed_zero_at; // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms @@ -94,7 +103,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { if (this->value == 65535) { // fully on, enable output immediately - this->gate_pin->digital_write(true); + this->gate_pin.digital_write(true); } else if (this->init_cycle) { // send a full cycle this->init_cycle = false; @@ -102,30 +111,30 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { this->disable_time_us = cycle_time_us; } else if (this->value == 0) { // fully off, disable output immediately - this->gate_pin->digital_write(false); + this->gate_pin.digital_write(false); } else { if (this->method == DIM_METHOD_TRAILING) { this->enable_time_us = 1; // cannot be 0 - this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535); + this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535); } else { // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic // also take into account min_power auto min_us = this->cycle_time_us * this->min_power / 1000; - this->enable_time_us = max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); + this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); if (this->method == DIM_METHOD_LEADING_PULSE) { // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone // this is for brightness near 99% - this->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); + this->disable_time_us = std::max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); } else { - this->gate_pin->digital_write(false); + this->gate_pin.digital_write(false); this->disable_time_us = this->cycle_time_us; } } } } -void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { - // Attaching pin interrupts on the same pin will override the previous interupt +void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { + // Attaching pin interrupts on the same pin will override the previous interrupt // However, the user expects that multiple dimmers sharing the same ZC pin will work. // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. @@ -138,11 +147,11 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store } } -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 // ESP32 implementation, uses basically the same code but needs to wrap // timer_interrupt() function to auto-reschedule -static hw_timer_t *dimmer_timer = nullptr; -void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } +static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } #endif void AcDimmer::setup() { @@ -171,15 +180,16 @@ void AcDimmer::setup() { if (setup_zero_cross_pin) { this->zero_cross_pin_->setup(); this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr(); - this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING); + this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, + gpio::INTERRUPT_FALLING_EDGE); } -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 // Uses ESP8266 waveform (soft PWM) class // PWM and AcDimmer can even run at the same time this way setTimer1Callback(&timer_interrupt); #endif -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 // 80 Divider -> 1 count=1µs dimmer_timer = timerBegin(0, 80, true); timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); @@ -215,3 +225,5 @@ void AcDimmer::dump_config() { } // namespace ac_dimmer } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h index 00da061cfd..fd1bbc28db 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.h +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -1,7 +1,9 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" namespace esphome { @@ -11,11 +13,11 @@ enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TR struct AcDimmerDataStore { /// Zero-cross pin - ISRInternalGPIOPin *zero_cross_pin; + ISRInternalGPIOPin zero_cross_pin; /// Zero-cross pin number - used to share ZC pin across multiple dimmers uint8_t zero_cross_pin_number; /// Output pin to write to - ISRInternalGPIOPin *gate_pin; + ISRInternalGPIOPin gate_pin; /// Value of the dimmer - 0 to 65535. uint16_t value; /// Minimum power for activation @@ -37,7 +39,7 @@ struct AcDimmerDataStore { void gpio_intr(); static void s_gpio_intr(AcDimmerDataStore *store); -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 static void s_timer_intr(); #endif }; @@ -47,16 +49,16 @@ class AcDimmer : public output::FloatOutput, public Component { void setup() override; void dump_config() override; - void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; } - void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } + void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; } + void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; } void set_method(DimMethod method) { method_ = method; } protected: void write_state(float state) override; - GPIOPin *gate_pin_; - GPIOPin *zero_cross_pin_; + InternalGPIOPin *gate_pin_; + InternalGPIOPin *zero_cross_pin_; AcDimmerDataStore store_; bool init_with_half_cycle_; DimMethod method_; @@ -64,3 +66,5 @@ class AcDimmer : public output::FloatOutput, public Component { } // namespace ac_dimmer } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 2c37d325eb..c39fc382b6 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -19,17 +19,20 @@ DIM_METHODS = { CONF_GATE_PIN = "gate_pin" CONF_ZERO_CROSS_PIN = "zero_cross_pin" CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle" -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.Required(CONF_ID): cv.declare_id(AcDimmer), - cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, - cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum( - DIM_METHODS, upper=True, space="_" - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(AcDimmer), + cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, + cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum( + DIM_METHODS, upper=True, space="_" + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) async def to_code(config): diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 63fd7c60cc..d9c2892d21 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -42,8 +42,9 @@ void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { - it[led].set(COLOR_BLACK); + it[led].set(Color::BLACK); } + it.schedule_show(); } void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { @@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); } + it.schedule_show(); return CONSUMED; } diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 960d9ed8e2..c8f8b0e0f6 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -1,8 +1,13 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC +#include ADC_MODE(ADC_VCC) +#else +#include +#endif #endif namespace esphome { @@ -10,44 +15,90 @@ namespace adc { static const char *const TAG = "adc"; -#ifdef ARDUINO_ARCH_ESP32 -void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; } +#ifdef USE_ESP32 +void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } + +inline adc1_channel_t gpio_to_adc1(uint8_t pin) { +#if CONFIG_IDF_TARGET_ESP32 + switch (pin) { + case 36: + return ADC1_CHANNEL_0; + case 37: + return ADC1_CHANNEL_1; + case 38: + return ADC1_CHANNEL_2; + case 39: + return ADC1_CHANNEL_3; + case 32: + return ADC1_CHANNEL_4; + case 33: + return ADC1_CHANNEL_5; + case 34: + return ADC1_CHANNEL_6; + case 35: + return ADC1_CHANNEL_7; + default: + return ADC1_CHANNEL_MAX; + } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (pin) { + case 0: + return ADC1_CHANNEL_0; + case 1: + return ADC1_CHANNEL_1; + case 2: + return ADC1_CHANNEL_2; + case 3: + return ADC1_CHANNEL_3; + case 4: + return ADC1_CHANNEL_4; + default: + return ADC1_CHANNEL_MAX; + } +#endif +} #endif void ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); #ifndef USE_ADC_SENSOR_VCC - GPIOPin(this->pin_, INPUT).setup(); + pin_->setup(); #endif -#ifdef ARDUINO_ARCH_ESP32 - analogSetPinAttenuation(this->pin_, this->attenuation_); +#ifdef USE_ESP32 + adc1_config_channel_atten(gpio_to_adc1(pin_->get_pin()), attenuation_); + adc1_config_width(ADC_WIDTH_BIT_12); +#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 + adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin())); +#endif #endif } void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC ESP_LOGCONFIG(TAG, " Pin: VCC"); #else - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + LOG_PIN(" Pin: ", pin_); #endif #endif -#ifdef ARDUINO_ARCH_ESP32 - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); +#ifdef USE_ESP32 + LOG_PIN(" Pin: ", pin_); switch (this->attenuation_) { - case ADC_0db: + case ADC_ATTEN_DB_0: ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); break; - case ADC_2_5db: + case ADC_ATTEN_DB_2_5: ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); break; - case ADC_6db: + case ADC_ATTEN_DB_6: ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); break; - case ADC_11db: + case ADC_ATTEN_DB_11: ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; } #endif LOG_UPDATE_INTERVAL(this); @@ -59,34 +110,56 @@ void ADCSensor::update() { this->publish_state(value_v); } float ADCSensor::sample() { -#ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; // NOLINT +#ifdef USE_ESP32 + int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin())); + float value_v = raw / 4095.0f; +#if CONFIG_IDF_TARGET_ESP32 switch (this->attenuation_) { - case ADC_0db: + case ADC_ATTEN_DB_0: value_v *= 1.1; break; - case ADC_2_5db: + case ADC_ATTEN_DB_2_5: value_v *= 1.5; break; - case ADC_6db: + case ADC_ATTEN_DB_6: value_v *= 2.2; break; - case ADC_11db: + case ADC_ATTEN_DB_11: value_v *= 3.9; break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (this->attenuation_) { + case ADC_ATTEN_DB_0: + value_v *= 0.84; + break; + case ADC_ATTEN_DB_2_5: + value_v *= 1.13; + break; + case ADC_ATTEN_DB_6: + value_v *= 1.56; + break; + case ADC_ATTEN_DB_11: + value_v *= 3.0; + break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; + } +#endif return value_v; #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC - return ESP.getVcc() / 1024.0f; + return ESP.getVcc() / 1024.0f; // NOLINT(readability-static-accessed-through-instance) #else - return analogRead(this->pin_) / 1024.0f; // NOLINT + return analogRead(this->pin_->get_pin()) / 1024.0f; // NOLINT #endif #endif } -#ifdef ARDUINO_ARCH_ESP8266 +#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 3a08ff6be4..b8c702be4e 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -1,19 +1,23 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.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" +#ifdef USE_ESP32 +#include "driver/adc.h" +#endif + namespace esphome { namespace adc { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { public: -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_attenuation_t attenuation); + void set_attenuation(adc_atten_t attenuation); #endif /// Update adc values. @@ -23,18 +27,18 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage void dump_config() override; /// `HARDWARE_LATE` setup priority. float get_setup_priority() const override; - void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } float sample() override; -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 std::string unique_id() override; #endif protected: - uint8_t pin_; + InternalGPIOPin *pin_; -#ifdef ARDUINO_ARCH_ESP32 - adc_attenuation_t attenuation_{ADC_0db}; +#ifdef USE_ESP32 + adc_atten_t attenuation_{ADC_ATTEN_DB_0}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 90561679b7..9a0407d0f4 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -5,29 +5,54 @@ from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_ATTENUATION, CONF_ID, + CONF_INPUT, CONF_PIN, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) +from esphome.core import CORE AUTO_LOAD = ["voltage_sampler"] ATTENUATION_MODES = { - "0db": cg.global_ns.ADC_0db, - "2.5db": cg.global_ns.ADC_2_5db, - "6db": cg.global_ns.ADC_6db, - "11db": cg.global_ns.ADC_11db, + "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, } def validate_adc_pin(value): - vcc = str(value).upper() - if vcc == "VCC": - return cv.only_on_esp8266(vcc) - return pins.analog_pin(value) + if str(value).upper() == "VCC": + return cv.only_on_esp8266("VCC") + + if CORE.is_esp32: + from esphome.components.esp32 import is_esp32c3 + + value = pins.internal_gpio_input_pin_number(value) + if is_esp32c3(): + if not (0 <= value <= 4): # ADC1 + raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") + if not (32 <= value <= 39): # ADC1 + raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") + elif CORE.is_esp8266: + from esphome.components.esp8266.gpio import CONF_ANALOG + + value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( + value + ) + + if value != 17: # A0 + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") + return pins.gpio_pin_schema( + {CONF_ANALOG: True, CONF_INPUT: True}, internal=True + )(value) + else: + raise NotImplementedError + + return pins.internal_gpio_input_pin_schema(value) adc_ns = cg.esphome_ns.namespace("adc") @@ -37,7 +62,10 @@ ADCSensor = adc_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -60,7 +88,8 @@ async def to_code(config): if config[CONF_PIN] == "VCC": cg.add_define("USE_ADC_SENSOR_VCC") else: - cg.add(var.set_pin(config[CONF_PIN])) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) if CONF_ATTENUATION in config: cg.add(var.set_attenuation(config[CONF_ATTENUATION])) diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index 0e6d5624c1..2c61fc6a44 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -8,9 +8,7 @@ static const char *const TAG = "ade7953"; void ADE7953::dump_config() { ESP_LOGCONFIG(TAG, "ADE7953:"); - if (this->has_irq_) { - ESP_LOGCONFIG(TAG, " IRQ Pin: GPIO%u", this->irq_pin_number_); - } + LOG_PIN(" IRQ Pin: ", irq_pin_); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); @@ -20,27 +18,28 @@ void ADE7953::dump_config() { LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_); } -#define ADE_PUBLISH_(name, factor) \ - if ((name) && this->name##_sensor_) { \ - float value = *(name) / (factor); \ +#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, factor) ADE_PUBLISH_(name, factor) +#define ADE_PUBLISH(name, val, factor) ADE_PUBLISH_(name, val, factor) void ADE7953::update() { if (!this->is_setup_) return; - auto active_power_a = this->ade_read_(0x0312); - ADE_PUBLISH(active_power_a, 154.0f); - auto active_power_b = this->ade_read_(0x0313); - ADE_PUBLISH(active_power_b, 154.0f); - auto current_a = this->ade_read_(0x031A); - ADE_PUBLISH(current_a, 100000.0f); - auto current_b = this->ade_read_(0x031B); - ADE_PUBLISH(current_b, 100000.0f); - auto voltage = this->ade_read_(0x031C); - ADE_PUBLISH(voltage, 26000.0f); + 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); diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h index e0fadf37c3..c6fb383ed8 100644 --- a/esphome/components/ade7953/ade7953.h +++ b/esphome/components/ade7953/ade7953.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" @@ -9,10 +10,7 @@ namespace ade7953 { class ADE7953 : public i2c::I2CDevice, public PollingComponent { public: - void set_irq_pin(uint8_t irq_pin) { - has_irq_ = true; - irq_pin_number_ = irq_pin; - } + 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; } @@ -24,15 +22,13 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { } void setup() override { - if (this->has_irq_) { - auto pin = GPIOPin(this->irq_pin_number_, INPUT); - this->irq_pin_ = &pin; + if (this->irq_pin_ != nullptr) { this->irq_pin_->setup(); } this->set_timeout(100, [this]() { - this->ade_write_(0x0010, 0x04); - this->ade_write_(0x00FE, 0xAD); - this->ade_write_(0x0120, 0x0030); + this->ade_write_8_(0x0010, 0x04); + this->ade_write_8_(0x00FE, 0xAD); + this->ade_write_16_(0x0120, 0x0030); this->is_setup_ = true; }); } @@ -42,31 +38,51 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { void update() override; protected: - template bool ade_write_(uint16_t reg, T value) { + i2c::ErrorCode ade_write_8_(uint16_t reg, uint8_t value) { std::vector data; data.push_back(reg >> 8); data.push_back(reg >> 0); - for (int i = sizeof(T) - 1; i >= 0; i--) - data.push_back(value >> (i * 8)); - return this->write_bytes_raw(data); + data.push_back(value); + return write(data.data(), data.size()); } - template optional ade_read_(uint16_t reg) { - uint8_t hi = reg >> 8; - uint8_t lo = reg >> 0; - if (!this->write_bytes_raw({hi, lo})) - return {}; - auto ret = this->read_bytes_raw(); - if (!ret.has_value()) - return {}; - T result = 0; - for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--) - result |= T((*ret)[i]) << (j * 8); - return result; + 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]) << 24; + *value |= ((uint32_t) recv[2]) << 24; + *value |= ((uint32_t) recv[3]) << 24; + return i2c::ERROR_OK; } - bool has_irq_ = false; - uint8_t irq_pin_number_; - GPIOPin *irq_pin_{nullptr}; + InternalGPIOPin *irq_pin_ = nullptr; bool is_setup_{false}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_a_sensor_{nullptr}; diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 90873f1a5e..d02f466091 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -30,29 +29,36 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(ADE7953), - cv.Optional(CONF_IRQ_PIN): pins.input_pin, + cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + 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_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + 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_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + 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_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -67,7 +73,8 @@ async def to_code(config): await i2c.register_i2c_device(var, config) if CONF_IRQ_PIN in config: - cg.add(var.set_irq_pin(config[CONF_IRQ_PIN])) + irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(irq_pin)) for key in [ CONF_VOLTAGE, diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index d33ac83813..beb379db93 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -1,5 +1,6 @@ #include "ads1115.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ads1115 { @@ -64,11 +65,6 @@ void ADS1115Component::setup() { return; } this->prev_config_ = config; - - for (auto *sensor : this->sensors_) { - this->set_interval(sensor->get_name(), sensor->update_interval(), - [this, sensor] { this->request_measurement(sensor); }); - } } void ADS1115Component::dump_config() { ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); @@ -107,17 +103,22 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { } this->prev_config_ = config; - // about 1.6 ms with 860 samples per second + // about 1.2 ms with 860 samples per second delay(2); - uint32_t start = millis(); - while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Reading ADS1115 timed out"); - this->status_set_warning(); - return NAN; + // in continuous mode, conversion will always be running, rely on the delay + // to ensure conversion is taking place with the correct settings + // can we use the rdy pin to trigger when a conversion is done? + if (!this->continuous_mode_) { + uint32_t start = millis(); + while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { + if (millis() - start > 100) { + ESP_LOGW(TAG, "Reading ADS1115 timed out"); + this->status_set_warning(); + return NAN; + } + yield(); } - yield(); } } @@ -159,7 +160,7 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); } void ADS1115Sensor::update() { float v = this->parent_->request_measurement(this); - if (!isnan(v)) { + if (!std::isnan(v)) { ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); this->publish_state(v); } diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index c521769279..da33a39041 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, CONF_ID, @@ -53,7 +52,10 @@ ADS1115Sensor = ads1115_ns.class_( CONF_ADS1115_ID = "ads1115_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 4688440d80..713199212c 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -10,10 +10,11 @@ // // According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost // immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best -// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. +// results making successive requests; the current implementation makes 3 attempts with a delay of 30ms each time. #include "aht10.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace aht10 { @@ -23,7 +24,7 @@ static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; 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_ATTEMPS = 3; // safety margin, normally 3 attemps are enough: 3*30=90ms +static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms void AHT10Component::setup() { ESP_LOGCONFIG(TAG, "Setting up AHT10..."); @@ -33,8 +34,19 @@ void AHT10Component::setup() { this->mark_failed(); return; } - uint8_t data; - if (!this->read_byte(0, &data, AHT10_DEFAULT_DELAY)) { + 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; @@ -55,15 +67,26 @@ void AHT10Component::update() { return; } uint8_t data[6]; - uint8_t delay = AHT10_DEFAULT_DELAY; + uint8_t delay_ms = AHT10_DEFAULT_DELAY; if (this->humidity_sensor_ != nullptr) - delay = AHT10_HUMIDITY_DELAY; - for (int i = 0; i < AHT10_ATTEMPS; ++i) { - ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); + 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_microseconds_accurate(4); - if (!this->read_bytes(0, data, 6, delay)) { + + uint8_t reg = 0; + if (this->write(®, 1) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); - } else if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy + continue; + } + 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) @@ -80,11 +103,12 @@ void AHT10Component::update() { } } else { // data is valid, we can break the loop - ESP_LOGVV(TAG, "Answer at %6ld", millis()); + ESP_LOGVV(TAG, "Answer at %6u", millis()); + success = true; break; } } - if ((data[0] & 0x80) == 0x80) { + if (!success || (data[0] & 0x80) == 0x80) { ESP_LOGE(TAG, "Measurements reading timed-out!"); this->status_set_warning(); return; @@ -105,7 +129,7 @@ void AHT10Component::update() { this->temperature_sensor_->publish_state(temperature); } if (this->humidity_sensor_ != nullptr) { - if (isnan(humidity)) + if (std::isnan(humidity)) ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); this->humidity_sensor_->publish_state(humidity); } diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index 35168be54a..654d645966 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AHT10Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py new file mode 100644 index 0000000000..ca94069703 --- /dev/null +++ b/esphome/components/airthings_ble/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32_ble_tracker"] +CODEOWNERS = ["@jeromelaban"] + +airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") +AirthingsListener = airthings_ble_ns.class_( + "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp new file mode 100644 index 0000000000..951961cb1b --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -0,0 +1,33 @@ +#include "airthings_listener.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace airthings_ble { + +static const char *const TAG = "airthings_ble"; + +bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + for (auto &it : device.get_manufacturer_datas()) { + if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) { + if (it.data.size() < 4) + continue; + + uint32_t sn = it.data[0]; + sn |= ((uint32_t) it.data[1] << 8); + 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()); + return true; + } + } + + return false; +} + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h new file mode 100644 index 0000000000..52f69ea970 --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +namespace esphome { +namespace airthings_ble { + +class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_wave_mini/__init__.py b/esphome/components/airthings_wave_mini/__init__.py new file mode 100644 index 0000000000..022f35b4cf --- /dev/null +++ b/esphome/components/airthings_wave_mini/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ncareau"] diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp new file mode 100644 index 0000000000..6b6418f7e6 --- /dev/null +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -0,0 +1,113 @@ +#include "airthings_wave_mini.h" + +#ifdef USE_ESP32 + +namespace esphome { +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()->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) { + 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)) { + 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()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void AirthingsWaveMini::dump_config() { + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); +} + +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)) {} + +} // namespace airthings_wave_mini +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h new file mode 100644 index 0000000000..128774f9cb --- /dev/null +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -0,0 +1,65 @@ +#pragma once + +#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_mini { + +static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; +static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; + +class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { + 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_; + + struct WaveMiniReadings { + uint16_t unused01; + uint16_t temperature; + uint16_t pressure; + uint16_t humidity; + uint16_t voc; + uint16_t unused02; + uint32_t unused03; + uint32_t unused04; + }; +}; + +} // namespace airthings_wave_mini +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_mini/sensor.py b/esphome/components/airthings_wave_mini/sensor.py new file mode 100644 index 0000000000..d38354fa84 --- /dev/null +++ b/esphome/components/airthings_wave_mini/sensor.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +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"] + +airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") +AirthingsWaveMini = airthings_wave_mini_ns.class_( + "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode +) + + +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), +) + + +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)) diff --git a/esphome/components/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py new file mode 100644 index 0000000000..1aff461edd --- /dev/null +++ b/esphome/components/airthings_wave_plus/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jeromelaban"] diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp new file mode 100644 index 0000000000..79f2cb7741 --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -0,0 +1,137 @@ +#include "airthings_wave_plus.h" + +#ifdef USE_ESP32 + +namespace esphome { +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()->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) { + 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); + + this->humidity_sensor_->publish_state(value->humidity / 2.0f); + if (is_valid_radon_value_(value->radon)) { + this->radon_sensor_->publish_state(value->radon); + } + if (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)) { + this->co2_sensor_->publish_state(value->co2); + } + if (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); + } else { + ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); + } + } +} + +bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && 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()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void AirthingsWavePlus::dump_config() { + 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_); +} + +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)) {} + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h new file mode 100644 index 0000000000..9dd6ed92d5 --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -0,0 +1,75 @@ +#pragma once + +#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_plus { + +static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; +static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; + +class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { + 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; } + + 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_(); + + 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_; + + struct WavePlusReadings { + uint8_t version; + uint8_t humidity; + uint8_t ambientLight; + uint8_t unused01; + uint16_t radon; + uint16_t radon_lt; + uint16_t temperature; + uint16_t pressure; + uint16_t co2; + uint16_t voc; + }; +}; + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py new file mode 100644 index 0000000000..727fbe15fb --- /dev/null +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -0,0 +1,116 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +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, +) + +DEPENDENCIES = ["ble_client"] + +airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") +AirthingsWavePlus = airthings_wave_plus_ns.class_( + "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode +) + + +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), +) + + +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_RADON in config: + sens = await sensor.new_sensor(config[CONF_RADON]) + cg.add(var.set_radon(sens)) + if CONF_RADON_LONG_TERM in config: + sens = await sensor.new_sensor(config[CONF_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]) + 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)) diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 7e8795dd30..b53eb69464 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -5,6 +5,7 @@ #include "am2320.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace am2320 { @@ -77,7 +78,7 @@ bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len if (conversion > 0) delay(conversion); - return this->parent_->raw_receive(this->address_, data, len); + return this->read(data, len) == i2c::ERROR_OK; } bool AM2320Component::read_data_(uint8_t *data) { diff --git a/esphome/components/am2320/sensor.py b/esphome/components/am2320/sensor.py index 5d6cb9eded..088978a8f1 100644 --- a/esphome/components/am2320/sensor.py +++ b/esphome/components/am2320/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AM2320Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/api/__init__.py b/esphome/components/am43/__init__.py similarity index 100% rename from esphome/api/__init__.py rename to esphome/components/am43/__init__.py diff --git a/esphome/components/am43/am43.cpp b/esphome/components/am43/am43.cpp new file mode 100644 index 0000000000..a62e3bb6df --- /dev/null +++ b/esphome/components/am43/am43.cpp @@ -0,0 +1,116 @@ +#include "am43.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace am43 { + +static const char *const TAG = "am43"; + +void Am43::dump_config() { + ESP_LOGCONFIG(TAG, "AM43"); + LOG_SENSOR(" ", "Battery", this->battery_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +void Am43::setup() { + this->encoder_ = make_unique(); + this->decoder_ = make_unique(); + this->logged_in_ = false; + this->last_battery_update_ = 0; + this->current_sensor_ = 0; +} + +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; + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->logged_in_ = false; + this->node_state = espbt::ClientState::IDLE; + if (this->battery_ != nullptr) + this->battery_->publish_state(NAN); + if (this->illuminance_ != nullptr) + this->illuminance_->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); + if (chr == nullptr) { + if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", + this->parent_->address_str().c_str()); + } else { + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", + this->parent_->address_str().c_str()); + } + break; + } + this->char_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->char_handle_) + break; + this->decoder_->decode(param->notify.value, param->notify.value_len); + + if (this->battery_ != nullptr && this->decoder_->has_battery_level() && + millis() - this->last_battery_update_ > 10000) { + this->battery_->publish_state(this->decoder_->battery_level_); + this->last_battery_update_ = millis(); + } + + if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) { + this->illuminance_->publish_state(this->decoder_->light_level_); + } + + if (this->current_sensor_ > 0) { + if (this->illuminance_ != nullptr) { + auto packet = this->encoder_->get_light_level_request(); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), + status); + } + this->current_sensor_ = 0; + } + break; + } + default: + break; + } +} + +void Am43::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->current_sensor_ == 0) { + if (this->battery_ != nullptr) { + auto packet = this->encoder_->get_battery_level_request(); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } + this->current_sensor_++; + } +} + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/am43.h b/esphome/components/am43/am43.h new file mode 100644 index 0000000000..8dfe83e3a3 --- /dev/null +++ b/esphome/components/am43/am43.h @@ -0,0 +1,45 @@ +#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/components/am43/am43_base.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace am43 { + +namespace espbt = esphome::esp32_ble_tracker; + +class Am43 : 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_battery(sensor::Sensor *battery) { battery_ = battery; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + + protected: + uint16_t char_handle_; + std::unique_ptr encoder_; + std::unique_ptr decoder_; + bool logged_in_; + sensor::Sensor *battery_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; + uint8_t current_sensor_; + // The AM43 often gets into a state where it spams loads of battery update + // notifications. Here we will limit to no more than every 10s. + uint8_t last_battery_update_; +}; + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/am43_base.cpp b/esphome/components/am43/am43_base.cpp new file mode 100644 index 0000000000..af474dcb79 --- /dev/null +++ b/esphome/components/am43/am43_base.cpp @@ -0,0 +1,144 @@ +#include "am43_base.h" +#include +#include + +namespace esphome { +namespace am43 { + +const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; + +std::string pkt_to_hex(const uint8_t *data, uint16_t len) { + char buf[64]; + memset(buf, 0, 64); + for (int i = 0; i < len; i++) + sprintf(&buf[i * 2], "%02x", data[i]); + std::string ret = buf; + return ret; +} + +Am43Packet *Am43Encoder::get_battery_level_request() { + uint8_t data = 0x1; + return this->encode_(0xA2, &data, 1); +} + +Am43Packet *Am43Encoder::get_light_level_request() { + uint8_t data = 0x1; + return this->encode_(0xAA, &data, 1); +} + +Am43Packet *Am43Encoder::get_position_request() { + uint8_t data = 0x1; + return this->encode_(CMD_GET_POSITION, &data, 1); +} + +Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) { + uint8_t data[2]; + data[0] = (pin & 0xFF00) >> 8; + data[1] = pin & 0xFF; + return this->encode_(CMD_SEND_PIN, data, 2); +} + +Am43Packet *Am43Encoder::get_open_request() { + uint8_t data = 0xDD; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_close_request() { + uint8_t data = 0xEE; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_stop_request() { + uint8_t data = 0xCC; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) { + return this->encode_(CMD_SET_POSITION, &position, 1); +} + +void Am43Encoder::checksum_() { + uint8_t checksum = 0; + int i = 0; + for (i = 0; i < this->packet_.length; i++) + checksum = checksum ^ this->packet_.data[i]; + this->packet_.data[i] = checksum ^ 0xff; + this->packet_.length++; +} + +Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) { + memcpy(this->packet_.data, START_PACKET, 5); + this->packet_.data[5] = command; + this->packet_.data[6] = length; + memcpy(&this->packet_.data[7], data, length); + this->packet_.length = length + 7; + this->checksum_(); + ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str()); + return &this->packet_; +} + +#define VERIFY_MIN_LENGTH(x) \ + if (length < (x)) \ + return; + +void Am43Decoder::decode(const uint8_t *data, uint16_t length) { + this->has_battery_level_ = false; + this->has_light_level_ = false; + this->has_set_position_response_ = false; + this->has_set_state_response_ = false; + this->has_position_ = false; + this->has_pin_response_ = false; + ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str()); + + if (length < 2 || data[0] != 0x9a) + return; + switch (data[1]) { + case CMD_GET_BATTERY_LEVEL: { + VERIFY_MIN_LENGTH(8); + this->battery_level_ = data[7]; + this->has_battery_level_ = true; + break; + } + case CMD_GET_LIGHT_LEVEL: { + VERIFY_MIN_LENGTH(5); + this->light_level_ = 100 * ((float) data[4] / 9); + this->has_light_level_ = true; + break; + } + case CMD_GET_POSITION: { + VERIFY_MIN_LENGTH(6); + this->position_ = data[5]; + this->has_position_ = true; + break; + } + case CMD_NOTIFY_POSITION: { + VERIFY_MIN_LENGTH(5); + this->position_ = data[4]; + this->has_position_ = true; + break; + } + case CMD_SEND_PIN: { + VERIFY_MIN_LENGTH(4); + this->pin_ok_ = data[3] == RESPONSE_ACK; + this->has_pin_response_ = true; + break; + } + case CMD_SET_POSITION: { + VERIFY_MIN_LENGTH(4); + this->set_position_ok_ = data[3] == RESPONSE_ACK; + this->has_set_position_response_ = true; + break; + } + case CMD_SET_STATE: { + VERIFY_MIN_LENGTH(4); + this->set_state_ok_ = data[3] == RESPONSE_ACK; + this->has_set_state_response_ = true; + break; + } + default: + break; + } +}; + +} // namespace am43 +} // namespace esphome diff --git a/esphome/components/am43/am43_base.h b/esphome/components/am43/am43_base.h new file mode 100644 index 0000000000..e817f161fe --- /dev/null +++ b/esphome/components/am43/am43_base.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace am43 { + +static const uint16_t AM43_SERVICE_UUID = 0xFE50; +static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51; +// +// Tuya identifiers, only to detect and warn users as they are incompatible. +static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910; +static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11; + +struct Am43Packet { + uint8_t length; + uint8_t data[24]; +}; + +static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2; +static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA; +static const uint8_t CMD_GET_POSITION = 0xA7; +static const uint8_t CMD_SEND_PIN = 0x17; +static const uint8_t CMD_SET_STATE = 0x0A; +static const uint8_t CMD_SET_POSITION = 0x0D; +static const uint8_t CMD_NOTIFY_POSITION = 0xA1; + +static const uint8_t RESPONSE_ACK = 0x5A; +static const uint8_t RESPONSE_NACK = 0xA5; + +class Am43Encoder { + public: + Am43Packet *get_battery_level_request(); + Am43Packet *get_light_level_request(); + Am43Packet *get_position_request(); + Am43Packet *get_send_pin_request(uint16_t pin); + Am43Packet *get_open_request(); + Am43Packet *get_close_request(); + Am43Packet *get_stop_request(); + Am43Packet *get_set_position_request(uint8_t position); + + protected: + void checksum_(); + Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length); + Am43Packet packet_; +}; + +class Am43Decoder { + public: + void decode(const uint8_t *data, uint16_t length); + bool has_battery_level() { return this->has_battery_level_; } + bool has_light_level() { return this->has_light_level_; } + bool has_set_position_response() { return this->has_set_position_response_; } + bool has_set_state_response() { return this->has_set_state_response_; } + bool has_position() { return this->has_position_; } + bool has_pin_response() { return this->has_pin_response_; } + + union { + uint8_t position_; + uint8_t battery_level_; + float light_level_; + uint8_t set_position_ok_; + uint8_t set_state_ok_; + uint8_t pin_ok_; + }; + + protected: + bool has_battery_level_; + bool has_light_level_; + bool has_set_position_response_; + bool has_set_state_response_; + bool has_position_; + bool has_pin_response_; +}; + +} // namespace am43 +} // namespace esphome diff --git a/esphome/components/am43/cover/__init__.py b/esphome/components/am43/cover/__init__.py new file mode 100644 index 0000000000..1ab0edbe78 --- /dev/null +++ b/esphome/components/am43/cover/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover, ble_client +from esphome.const import CONF_ID, CONF_PIN + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["ble_client"] +AUTO_LOAD = ["am43"] + +CONF_INVERT_POSITION = "invert_position" + +am43_ns = cg.esphome_ns.namespace("am43") +Am43Component = am43_ns.class_( + "Am43Component", cover.Cover, ble_client.BLEClientNode, cg.Component +) + +CONFIG_SCHEMA = ( + cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Am43Component), + cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), + cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) + yield cg.register_component(var, config) + yield cover.register_cover(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp new file mode 100644 index 0000000000..274c527760 --- /dev/null +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -0,0 +1,149 @@ +#include "am43_cover.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace am43 { + +static const char *const TAG = "am43_cover"; + +using namespace esphome::cover; + +void Am43Component::dump_config() { + LOG_COVER("", "AM43 Cover", this); + ESP_LOGCONFIG(TAG, " Device Pin: %d", this->pin_); + ESP_LOGCONFIG(TAG, " Invert Position: %d", (int) this->invert_position_); +} + +void Am43Component::setup() { + this->position = COVER_OPEN; + this->encoder_ = make_unique(); + this->decoder_ = make_unique(); + this->logged_in_ = false; +} + +void Am43Component::loop() { + if (this->node_state == espbt::ClientState::ESTABLISHED && !this->logged_in_) { + auto packet = this->encoder_->get_send_pin_request(this->pin_); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str()); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status); + else + this->logged_in_ = true; + } +} + +CoverTraits Am43Component::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_position(true); + traits.set_supports_tilt(false); + traits.set_is_assumed_state(false); + return traits; +} + +void Am43Component::control(const CoverCall &call) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str()); + return; + } + if (call.get_stop()) { + auto packet = this->encoder_->get_stop_request(); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + + if (this->invert_position_) + pos = 1 - pos; + auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status); + } +} + +void Am43Component::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_DISCONNECT_EVT: { + this->logged_in_ = false; + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); + if (chr == nullptr) { + if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str()); + } else { + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->get_name().c_str()); + } + break; + } + this->char_handle_ = chr->handle; + + auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_) + break; + this->decoder_->decode(param->notify.value, param->notify.value_len); + + if (this->decoder_->has_position()) { + this->position = ((float) this->decoder_->position_ / 100.0); + if (!this->invert_position_) + this->position = 1 - this->position; + if (this->position > 0.97) + this->position = 1.0; + if (this->position < 0.02) + this->position = 0.0; + this->publish_state(); + } + + if (this->decoder_->has_pin_response()) { + if (this->decoder_->pin_ok_) { + ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str()); + auto packet = this->encoder_->get_position_request(); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status); + } else { + ESP_LOGW(TAG, "[%s] AM43 pin rejected!", this->get_name().c_str()); + } + } + + if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_) + ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", this->get_name().c_str()); + + if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_) + ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", this->get_name().c_str()); + break; + } + default: + break; + } +} + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h new file mode 100644 index 0000000000..f33f2d1734 --- /dev/null +++ b/esphome/components/am43/cover/am43_cover.h @@ -0,0 +1,45 @@ +#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/cover/cover.h" +#include "esphome/components/am43/am43_base.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace am43 { + +namespace espbt = esphome::esp32_ble_tracker; + +class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component { + public: + void setup() override; + 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; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + cover::CoverTraits get_traits() override; + void set_pin(uint16_t pin) { this->pin_ = pin; } + void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } + + protected: + void control(const cover::CoverCall &call) override; + uint16_t char_handle_; + uint16_t pin_; + bool invert_position_; + std::unique_ptr encoder_; + std::unique_ptr decoder_; + bool logged_in_; + + float position_; +}; + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/sensor.py b/esphome/components/am43/sensor.py new file mode 100644 index 0000000000..c88e529a0c --- /dev/null +++ b/esphome/components/am43/sensor.py @@ -0,0 +1,46 @@ +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_BATTERY_LEVEL, + ICON_BATTERY, + CONF_ILLUMINANCE, + ICON_BRIGHTNESS_5, + UNIT_PERCENT, +) + +CODEOWNERS = ["@buxtronix"] + +am43_ns = cg.esphome_ns.namespace("am43") +Am43 = am43_ns.class_("Am43", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Am43), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, ICON_BATTERY, 0 + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + UNIT_PERCENT, ICON_BRIGHTNESS_5, 0 + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("120s")) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield ble_client.register_ble_node(var, config) + + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery(sens)) + + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 3ae3aa94f9..3f03e5c185 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -5,7 +5,7 @@ from esphome.components import display, font import esphome.components.image as espImage import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE +from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) @@ -15,8 +15,6 @@ MULTI_CONF = True Animation_ = display.display_ns.class_("Animation") -CONF_RAW_DATA_ID = "raw_data_id" - ANIMATION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Animation_), diff --git a/esphome/components/xiaomi_miflora/__init__.py b/esphome/components/anova/__init__.py similarity index 100% rename from esphome/components/xiaomi_miflora/__init__.py rename to esphome/components/anova/__init__.py diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp new file mode 100644 index 0000000000..5d9afddc74 --- /dev/null +++ b/esphome/components/anova/anova.cpp @@ -0,0 +1,150 @@ +#include "anova.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace anova { + +static const char *const TAG = "anova"; + +using namespace esphome::climate; + +void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); } + +void Anova::setup() { + this->codec_ = make_unique(); + this->current_request_ = 0; +} + +void Anova::loop() {} + +void Anova::control(const ClimateCall &call) { + if (call.get_mode().has_value()) { + ClimateMode mode = *call.get_mode(); + AnovaPacket *pkt; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + pkt = this->codec_->get_stop_request(); + break; + case climate::CLIMATE_MODE_HEAT: + pkt = this->codec_->get_start_request(); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %d", mode); + return; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } + if (call.get_target_temperature().has_value()) { + auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } +} + +void Anova::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_DISCONNECT_EVT: { + this->current_temperature = NAN; + this->target_temperature = NAN; + this->publish_state(); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); + break; + } + this->char_handle_ = chr->handle; + + auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + this->current_request_ = 0; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_) + break; + this->codec_->decode(param->notify.value, param->notify.value_len); + if (this->codec_->has_target_temp()) { + this->target_temperature = this->codec_->target_temp_; + } + if (this->codec_->has_current_temp()) { + this->current_temperature = this->codec_->current_temp_; + } + if (this->codec_->has_running()) { + this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; + } + if (this->codec_->has_unit()) { + this->fahrenheit_ = (this->codec_->unit_ == 'f'); + ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius"); + this->current_request_++; + } + this->publish_state(); + + if (this->current_request_ > 1) { + AnovaPacket *pkt = nullptr; + switch (this->current_request_++) { + case 2: + pkt = this->codec_->get_read_target_temp_request(); + break; + case 3: + pkt = this->codec_->get_read_current_temp_request(); + break; + default: + this->current_request_ = 1; + break; + } + if (pkt != nullptr) { + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, + pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), + status); + } + } + break; + } + default: + break; + } +} + +void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); } + +void Anova::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) + return; + + if (this->current_request_ < 2) { + auto pkt = this->codec_->get_read_device_status_request(); + if (this->current_request_ == 0) + this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + this->current_request_++; + } +} + +} // namespace anova +} // namespace esphome + +#endif diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h new file mode 100644 index 0000000000..2e6910f326 --- /dev/null +++ b/esphome/components/anova/anova.h @@ -0,0 +1,52 @@ +#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/climate/climate.h" +#include "anova_base.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace anova { + +namespace espbt = esphome::esp32_ble_tracker; + +static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0; +static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1; + +class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void loop() 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; } + climate::ClimateTraits traits() override { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_heat_mode(true); + traits.set_visual_min_temperature(25.0); + traits.set_visual_max_temperature(100.0); + traits.set_visual_temperature_step(0.1); + return traits; + } + void set_unit_of_measurement(const char *); + + protected: + std::unique_ptr codec_; + void control(const climate::ClimateCall &call) override; + uint16_t char_handle_; + uint8_t current_request_; + bool fahrenheit_; +}; + +} // namespace anova +} // namespace esphome + +#endif diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp new file mode 100644 index 0000000000..811a34a27a --- /dev/null +++ b/esphome/components/anova/anova_base.cpp @@ -0,0 +1,139 @@ +#include "anova_base.h" +#include +#include + +namespace esphome { +namespace anova { + +float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); } + +float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; } + +AnovaPacket *AnovaCodec::clean_packet_() { + this->packet_.length = strlen((char *) this->packet_.data); + this->packet_.data[this->packet_.length] = '\0'; + ESP_LOGV("anova", "SendPkt: %s\n", this->packet_.data); + return &this->packet_; +} + +AnovaPacket *AnovaCodec::get_read_device_status_request() { + this->current_query_ = READ_DEVICE_STATUS; + sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_target_temp_request() { + this->current_query_ = READ_TARGET_TEMPERATURE; + sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_current_temp_request() { + this->current_query_ = READ_CURRENT_TEMPERATURE; + sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_unit_request() { + this->current_query_ = READ_UNIT; + sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_data_request() { + this->current_query_ = READ_DATA; + sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) { + this->current_query_ = SET_TARGET_TEMPERATURE; + if (this->fahrenheit_) + temperature = ctof(temperature); + sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { + this->current_query_ = SET_UNIT; + sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_start_request() { + this->current_query_ = START; + sprintf((char *) this->packet_.data, CMD_START); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_stop_request() { + this->current_query_ = STOP; + sprintf((char *) this->packet_.data, CMD_STOP); + return this->clean_packet_(); +} + +void AnovaCodec::decode(const uint8_t *data, uint16_t length) { + memset(this->buf_, 0, 32); + strncpy(this->buf_, (char *) data, length); + this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false; + switch (this->current_query_) { + case READ_DEVICE_STATUS: { + if (!strncmp(this->buf_, "stopped", 7)) { + this->has_running_ = true; + this->running_ = false; + } + if (!strncmp(this->buf_, "running", 7)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case START: { + if (!strncmp(this->buf_, "start", 5)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case STOP: { + if (!strncmp(this->buf_, "stop", 4)) { + this->has_running_ = true; + this->running_ = false; + } + break; + } + case READ_TARGET_TEMPERATURE: { + this->target_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->target_temp_ = ftoc(this->target_temp_); + this->has_target_temp_ = true; + break; + } + case SET_TARGET_TEMPERATURE: { + this->target_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->target_temp_ = ftoc(this->target_temp_); + this->has_target_temp_ = true; + break; + } + case READ_CURRENT_TEMPERATURE: { + this->current_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->current_temp_ = ftoc(this->current_temp_); + this->has_current_temp_ = true; + break; + } + case SET_UNIT: + case READ_UNIT: { + this->unit_ = this->buf_[0]; + this->fahrenheit_ = this->buf_[0] == 'f'; + this->has_unit_ = true; + break; + } + default: + break; + } +} + +} // namespace anova +} // namespace esphome diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h new file mode 100644 index 0000000000..7c1383512d --- /dev/null +++ b/esphome/components/anova/anova_base.h @@ -0,0 +1,80 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace anova { + +enum CurrentQuery { + NONE, + READ_DEVICE_STATUS, + READ_TARGET_TEMPERATURE, + READ_CURRENT_TEMPERATURE, + READ_DATA, + READ_UNIT, + SET_TARGET_TEMPERATURE, + SET_UNIT, + START, + STOP, +}; + +struct AnovaPacket { + uint16_t length; + uint8_t data[24]; +}; + +#define CMD_READ_DEVICE_STATUS "status\r" +#define CMD_READ_TARGET_TEMP "read set temp\r" +#define CMD_READ_CURRENT_TEMP "read temp\r" +#define CMD_READ_UNIT "read unit\r" +#define CMD_READ_DATA "read data\r" +#define CMD_SET_TARGET_TEMP "set temp %.1f\r" +#define CMD_SET_TEMP_UNIT "set unit %c\r" + +#define CMD_START "start\r" +#define CMD_STOP "stop\r" + +class AnovaCodec { + public: + AnovaPacket *get_read_device_status_request(); + AnovaPacket *get_read_target_temp_request(); + AnovaPacket *get_read_current_temp_request(); + AnovaPacket *get_read_data_request(); + AnovaPacket *get_read_unit_request(); + + AnovaPacket *get_set_target_temp_request(float temperature); + AnovaPacket *get_set_unit_request(char unit); + + AnovaPacket *get_start_request(); + AnovaPacket *get_stop_request(); + + void decode(const uint8_t *data, uint16_t length); + bool has_target_temp() { return this->has_target_temp_; } + bool has_current_temp() { return this->has_current_temp_; } + bool has_unit() { return this->has_unit_; } + bool has_running() { return this->has_running_; } + + union { + float target_temp_; + float current_temp_; + char unit_; + bool running_; + }; + + protected: + AnovaPacket *clean_packet_(); + AnovaPacket packet_; + + bool has_target_temp_; + bool has_current_temp_; + bool has_unit_; + bool has_running_; + char buf_[32]; + bool fahrenheit_; + + CurrentQuery current_query_; +}; + +} // namespace anova +} // namespace esphome diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py new file mode 100644 index 0000000000..bdd77d6a33 --- /dev/null +++ b/esphome/components/anova/climate.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, ble_client +from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT + +UNITS = { + "f": "f", + "c": "c", +} + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["ble_client"] + +anova_ns = cg.esphome_ns.namespace("anova") +Anova = anova_ns.class_( + "Anova", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Anova), + cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), + } + ) + .extend(ble_client.BLE_CLIENT_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) + await climate.register_climate(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 15b0cb39f8..9ee873ac64 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -1,5 +1,6 @@ #include "apds9960.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace apds9960 { diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py index cb0c52735d..e1990ec26e 100644 --- a/esphome/components/apds9960/sensor.py +++ b/esphome/components/apds9960/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_TYPE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_LIGHTBULB, @@ -21,7 +20,10 @@ TYPES = { } CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 559f8f649c..b0608a69dd 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,3 +1,5 @@ +import base64 + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -6,6 +8,7 @@ from esphome.const import ( CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, + CONF_KEY, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, @@ -19,7 +22,7 @@ from esphome.const import ( from esphome.core import coroutine_with_priority DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@OttoWinter"] api_ns = cg.esphome_ns.namespace("api") @@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = { "float[]": cg.std_vector.template(float), "string[]": cg.std_vector.template(cg.std_string), } +CONF_ENCRYPTION = "encryption" + + +def validate_encryption_key(value): + value = cv.string_strict(value) + try: + decoded = base64.b64decode(value, validate=True) + except ValueError as err: + raise cv.Invalid("Invalid key format, please check it's using base64") from err + + if len(decoded) != 32: + raise cv.Invalid("Encryption key must be base64 and 32 bytes long") + + # Return original data for roundtrip conversion + return value + CONFIG_SCHEMA = cv.Schema( { @@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema( ), } ), + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -92,6 +116,15 @@ async def to_code(config): 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]) + cg.add(var.set_noise_psk(list(decoded))) + cg.add_define("USE_API_NOISE") + cg.add_library("esphome/noise-c", "0.1.3") + else: + cg.add_define("USE_API_PLAINTEXT") + cg.add_define("USE_API") cg.add_global(api_ns.using) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index bdb94b3d9b..5a6eba004c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -38,6 +38,8 @@ service APIConnection { rpc switch_command (SwitchCommandRequest) returns (void) {} rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} + rpc number_command (NumberCommandRequest) returns (void) {} + rpc select_command (SelectCommandRequest) returns (void) {} } @@ -212,6 +214,8 @@ message ListEntitiesBinarySensorResponse { string device_class = 5; bool is_status_binary_sensor = 6; + bool disabled_by_default = 7; + string icon = 8; } message BinarySensorStateResponse { option (id) = 21; @@ -241,6 +245,8 @@ message ListEntitiesCoverResponse { bool supports_position = 6; bool supports_tilt = 7; string device_class = 8; + bool disabled_by_default = 9; + string icon = 10; } enum LegacyCoverState { @@ -308,6 +314,8 @@ message ListEntitiesFanResponse { bool supports_speed = 6; bool supports_direction = 7; int32 supported_speed_count = 8; + bool disabled_by_default = 9; + string icon = 10; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -351,6 +359,18 @@ message FanCommandRequest { } // ==================== LIGHT ==================== +enum ColorMode { + COLOR_MODE_UNKNOWN = 0; + COLOR_MODE_ON_OFF = 1; + COLOR_MODE_BRIGHTNESS = 2; + COLOR_MODE_WHITE = 7; + COLOR_MODE_COLOR_TEMPERATURE = 11; + COLOR_MODE_COLD_WARM_WHITE = 19; + COLOR_MODE_RGB = 35; + COLOR_MODE_RGB_WHITE = 39; + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; + COLOR_MODE_RGB_COLD_WARM_WHITE = 51; +} message ListEntitiesLightResponse { option (id) = 15; option (source) = SOURCE_SERVER; @@ -361,13 +381,17 @@ message ListEntitiesLightResponse { string name = 3; string unique_id = 4; - bool supports_brightness = 5; - bool supports_rgb = 6; - bool supports_white_value = 7; - bool supports_color_temperature = 8; + repeated ColorMode supported_color_modes = 12; + // next four supports_* are for legacy clients, newer clients should use color modes + bool legacy_supports_brightness = 5 [deprecated=true]; + bool legacy_supports_rgb = 6 [deprecated=true]; + bool legacy_supports_white_value = 7 [deprecated=true]; + bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; repeated string effects = 11; + bool disabled_by_default = 13; + string icon = 14; } message LightStateResponse { option (id) = 24; @@ -378,11 +402,15 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + ColorMode color_mode = 11; + float color_brightness = 10; float red = 4; float green = 5; float blue = 6; float white = 7; float color_temperature = 8; + float cold_white = 12; + float warm_white = 13; string effect = 9; } message LightCommandRequest { @@ -396,6 +424,10 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_mode = 22; + ColorMode color_mode = 23; + bool has_color_brightness = 20; + float color_brightness = 21; bool has_rgb = 6; float red = 7; float green = 8; @@ -404,6 +436,10 @@ message LightCommandRequest { float white = 11; bool has_color_temperature = 12; float color_temperature = 13; + bool has_cold_white = 24; + float cold_white = 25; + bool has_warm_white = 26; + float warm_white = 27; bool has_transition_length = 14; uint32 transition_length = 15; bool has_flash_length = 16; @@ -416,6 +452,13 @@ message LightCommandRequest { enum SensorStateClass { STATE_CLASS_NONE = 0; STATE_CLASS_MEASUREMENT = 1; + STATE_CLASS_TOTAL_INCREASING = 2; +} + +enum SensorLastResetType { + LAST_RESET_NONE = 0; + LAST_RESET_NEVER = 1; + LAST_RESET_AUTO = 2; } message ListEntitiesSensorResponse { @@ -434,6 +477,9 @@ message ListEntitiesSensorResponse { bool force_update = 8; string device_class = 9; SensorStateClass state_class = 10; + // Last reset type removed in 2021.9.0 + SensorLastResetType legacy_last_reset_type = 11; + bool disabled_by_default = 12; } message SensorStateResponse { option (id) = 25; @@ -461,6 +507,7 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; + bool disabled_by_default = 7; } message SwitchStateResponse { option (id) = 26; @@ -493,6 +540,7 @@ message ListEntitiesTextSensorResponse { string unique_id = 4; string icon = 5; + bool disabled_by_default = 6; } message TextSensorStateResponse { option (id) = 27; @@ -513,9 +561,10 @@ enum LogLevel { LOG_LEVEL_ERROR = 1; LOG_LEVEL_WARN = 2; LOG_LEVEL_INFO = 3; - LOG_LEVEL_DEBUG = 4; - LOG_LEVEL_VERBOSE = 5; - LOG_LEVEL_VERY_VERBOSE = 6; + LOG_LEVEL_CONFIG = 4; + LOG_LEVEL_DEBUG = 5; + LOG_LEVEL_VERBOSE = 6; + LOG_LEVEL_VERY_VERBOSE = 7; } message SubscribeLogsRequest { option (id) = 28; @@ -530,7 +579,6 @@ message SubscribeLogsResponse { option (no_delay) = false; LogLevel level = 1; - string tag = 2; string message = 3; bool send_failed = 4; } @@ -652,6 +700,7 @@ message ListEntitiesCameraResponse { fixed32 key = 2; string name = 3; string unique_id = 4; + bool disabled_by_default = 5; } message CameraImageResponse { @@ -710,13 +759,14 @@ enum ClimateAction { CLIMATE_ACTION_FAN = 6; } enum ClimatePreset { - CLIMATE_PRESET_ECO = 0; - CLIMATE_PRESET_AWAY = 1; - CLIMATE_PRESET_BOOST = 2; - CLIMATE_PRESET_COMFORT = 3; - CLIMATE_PRESET_HOME = 4; - CLIMATE_PRESET_SLEEP = 5; - CLIMATE_PRESET_ACTIVITY = 6; + CLIMATE_PRESET_NONE = 0; + CLIMATE_PRESET_HOME = 1; + CLIMATE_PRESET_AWAY = 2; + CLIMATE_PRESET_BOOST = 3; + CLIMATE_PRESET_COMFORT = 4; + CLIMATE_PRESET_ECO = 5; + CLIMATE_PRESET_SLEEP = 6; + CLIMATE_PRESET_ACTIVITY = 7; } message ListEntitiesClimateResponse { option (id) = 46; @@ -734,13 +784,17 @@ message ListEntitiesClimateResponse { float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_temperature_step = 10; - bool supports_away = 11; + // for older peer versions - in new system this + // is if CLIMATE_PRESET_AWAY exists is supported_presets + bool legacy_supports_away = 11; bool supports_action = 12; repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateSwingMode supported_swing_modes = 14; repeated string supported_custom_fan_modes = 15; repeated ClimatePreset supported_presets = 16; repeated string supported_custom_presets = 17; + bool disabled_by_default = 18; + string icon = 19; } message ClimateStateResponse { option (id) = 47; @@ -754,7 +808,8 @@ message ClimateStateResponse { float target_temperature = 4; float target_temperature_low = 5; float target_temperature_high = 6; - bool away = 7; + // For older peers, equal to preset == CLIMATE_PRESET_AWAY + bool legacy_away = 7; ClimateAction action = 8; ClimateFanMode fan_mode = 9; ClimateSwingMode swing_mode = 10; @@ -777,8 +832,9 @@ message ClimateCommandRequest { float target_temperature_low = 7; bool has_target_temperature_high = 8; float target_temperature_high = 9; - bool has_away = 10; - bool away = 11; + // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset + bool has_legacy_away = 10; + bool legacy_away = 11; bool has_fan_mode = 12; ClimateFanMode fan_mode = 13; bool has_swing_mode = 14; @@ -790,3 +846,79 @@ message ClimateCommandRequest { bool has_custom_preset = 20; string custom_preset = 21; } + +// ==================== NUMBER ==================== +message ListEntitiesNumberResponse { + option (id) = 49; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + float min_value = 6; + float max_value = 7; + float step = 8; + bool disabled_by_default = 9; +} +message NumberStateResponse { + option (id) = 50; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; + // If the number does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message NumberCommandRequest { + option (id) = 51; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; +} + +// ==================== SELECT ==================== +message ListEntitiesSelectResponse { + option (id) = 52; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + repeated string options = 6; + bool disabled_by_default = 7; +} +message SelectStateResponse { + option (id) = 53; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the select does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message SelectCommandRequest { + option (id) = 54; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5576ace33b..47171ba50f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,7 +1,10 @@ #include "api_connection.h" +#include "esphome/core/entity_base.h" #include "esphome/core/log.h" -#include "esphome/core/util.h" +#include "esphome/components/network/util.h" #include "esphome/core/version.h" +#include "esphome/core/hal.h" +#include #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -18,143 +21,144 @@ namespace api { static const char *const TAG = "api.connection"; -APIConnection::APIConnection(AsyncClient *client, APIServer *parent) - : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { - this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); - this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); - this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, - this); - this->client_->onData([](void *s, AsyncClient *c, void *buf, - size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast(buf), len); }, - this); +APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) + : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + this->proto_write_buffer_.reserve(64); - this->send_buffer_.reserve(64); - this->recv_buffer_.reserve(32); - this->client_info_ = this->client_->remoteIP().toString().c_str(); +#if defined(USE_API_PLAINTEXT) + 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())}; +#else +#error "No frame helper defined" +#endif +} +void APIConnection::start() { this->last_traffic_ = millis(); -} -APIConnection::~APIConnection() { delete this->client_; } -void APIConnection::on_error_(int8_t error) { this->remove_ = true; } -void APIConnection::on_disconnect_() { this->remove_ = true; } -void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); } -void APIConnection::on_data_(uint8_t *buf, size_t len) { - if (len == 0 || buf == nullptr) + + APIError err = 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); return; - this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len); -} -void APIConnection::parse_recv_buffer_() { - if (this->recv_buffer_.empty() || this->remove_) - return; - - while (!this->recv_buffer_.empty()) { - if (this->recv_buffer_[0] != 0x00) { - ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); - this->on_fatal_error(); - return; - } - uint32_t i = 1; - const uint32_t size = this->recv_buffer_.size(); - uint32_t consumed; - auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_size_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_size = msg_size_varint->as_uint32(); - - auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_type_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_type = msg_type_varint->as_uint32(); - - if (size - i < msg_size) - // message body not fully received - return; - - uint8_t *msg = &this->recv_buffer_[i]; - this->read_message(msg_size, msg_type, msg); - if (this->remove_) - return; - // pop front - uint32_t total = i + msg_size; - this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); - this->last_traffic_ = millis(); } -} - -void APIConnection::disconnect_client() { - this->client_->close(); - this->remove_ = true; + client_info_ = helper_->getpeername(); + helper_->set_log_info(client_info_); } void APIConnection::loop() { if (this->remove_) return; - if (this->next_close_) { - this->disconnect_client(); - return; - } - - if (!network_is_connected()) { + if (!network::is_connected()) { // 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()); return; } - if (this->client_->disconnected()) { - // failsafe for disconnect logic - this->on_disconnect_(); + if (this->next_close_) { + // requested a disconnect + this->helper_->close(); + this->remove_ = true; return; } - this->parse_recv_buffer_(); + + APIError err = 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); + return; + } + ReadPacketBuffer buffer; + err = 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()); + } else { + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + } + return; + } else { + this->last_traffic_ = millis(); + // read a packet + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + if (this->remove_) + return; + } this->list_entities_iterator_.advance(); this->initial_state_iterator_.advance(); const uint32_t keepalive = 60000; + const uint32_t now = millis(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { - ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); - this->disconnect_client(); + 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()); } - } else if (millis() - this->last_traffic_ > keepalive) { + } else if (now - this->last_traffic_ > keepalive) { this->sent_ping_ = true; this->send_ping_request(PingRequest()); } #ifdef USE_ESP32_CAMERA - if (this->image_reader_.available()) { - uint32_t space = this->client_->space(); - // reserve 15 bytes for metadata, and at least 64 bytes of data - if (space >= 15 + 64) { - uint32_t to_send = std::min(space - 15, this->image_reader_.available()); - auto buffer = this->create_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); - // bool done = 3; - bool done = this->image_reader_.available() == to_send; - buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { + uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); + auto buffer = this->create_buffer(); + // fixed32 key = 1; + buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + // bytes data = 2; + buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + // bool done = 3; + bool done = this->image_reader_.available() == to_send; + buffer.encode_bool(3, done); + bool success = this->send_buffer(buffer, 44); - if (success) { - this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); - } + if (success) { + this->image_reader_.consume_data(to_send); + } + if (success && done) { + this->image_reader_.return_image(); } } #endif + + if (state_subs_at_ != -1) { + const auto &subs = this->parent_->get_state_subs(); + if (state_subs_at_ >= subs.size()) { + state_subs_at_ = -1; + } else { + auto &it = subs[state_subs_at_]; + SubscribeHomeAssistantStateResponse resp; + resp.entity_id = it.entity_id; + resp.attribute = it.attribute.value(); + if (this->send_subscribe_home_assistant_state_response(resp)) { + state_subs_at_++; + } + } + } } -std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { - return App.get_name() + component_type + nameable->get_object_id(); +std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) { + return App.get_name() + component_type + entity->get_object_id(); +} + +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()); + this->next_close_ = true; + DisconnectResponse resp; + return resp; +} +void APIConnection::on_disconnect_response(const DisconnectResponse &value) { + // pass } #ifdef USE_BINARY_SENSOR @@ -176,6 +180,8 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); msg.device_class = binary_sensor->get_device_class(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); + msg.disabled_by_default = binary_sensor->is_disabled_by_default(); + msg.icon = binary_sensor->get_icon(); return this->send_list_entities_binary_sensor_response(msg); } #endif @@ -207,6 +213,8 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.device_class = cover->get_device_class(); + msg.disabled_by_default = cover->is_disabled_by_default(); + msg.icon = cover->get_icon(); return this->send_list_entities_cover_response(msg); } void APIConnection::cover_command(const CoverCommandRequest &msg) { @@ -239,6 +247,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #endif #ifdef USE_FAN +// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" bool APIConnection::send_fan_state(fan::FanState *fan) { if (!this->state_subscription_) return false; @@ -268,6 +279,8 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + msg.disabled_by_default = fan->is_disabled_by_default(); + msg.icon = fan->get_icon(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -292,6 +305,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { call.set_direction(static_cast(msg.direction)); call.perform(); } +#pragma GCC diagnostic pop #endif #ifdef USE_LIGHT @@ -301,21 +315,21 @@ bool APIConnection::send_light_state(light::LightState *light) { auto traits = light->get_traits(); auto values = light->remote_values; + auto color_mode = values.get_color_mode(); LightStateResponse resp{}; resp.key = light->get_object_id_hash(); resp.state = values.is_on(); - if (traits.get_supports_brightness()) - resp.brightness = values.get_brightness(); - if (traits.get_supports_rgb()) { - resp.red = values.get_red(); - resp.green = values.get_green(); - resp.blue = values.get_blue(); - } - if (traits.get_supports_rgb_white_value()) - resp.white = values.get_white(); - if (traits.get_supports_color_temperature()) - resp.color_temperature = values.get_color_temperature(); + resp.color_mode = static_cast(color_mode); + resp.brightness = values.get_brightness(); + resp.color_brightness = values.get_color_brightness(); + resp.red = values.get_red(); + resp.green = values.get_green(); + resp.blue = values.get_blue(); + resp.white = values.get_white(); + resp.color_temperature = values.get_color_temperature(); + resp.cold_white = values.get_cold_white(); + resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); return this->send_light_state_response(resp); @@ -327,11 +341,22 @@ bool APIConnection::send_light_info(light::LightState *light) { msg.object_id = light->get_object_id(); msg.name = light->get_name(); msg.unique_id = get_default_unique_id("light", light); - msg.supports_brightness = traits.get_supports_brightness(); - msg.supports_rgb = traits.get_supports_rgb(); - msg.supports_white_value = traits.get_supports_rgb_white_value(); - msg.supports_color_temperature = traits.get_supports_color_temperature(); - if (msg.supports_color_temperature) { + + msg.disabled_by_default = light->is_disabled_by_default(); + msg.icon = light->get_icon(); + + for (auto mode : traits.get_supported_color_modes()) + msg.supported_color_modes.push_back(static_cast(mode)); + + msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); + msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); + msg.legacy_supports_white_value = + msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); + msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); + + if (msg.legacy_supports_color_temperature) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -352,6 +377,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_state(msg.state); if (msg.has_brightness) call.set_brightness(msg.brightness); + if (msg.has_color_mode) + call.set_color_mode(static_cast(msg.color_mode)); + if (msg.has_color_brightness) + call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { call.set_red(msg.red); call.set_green(msg.green); @@ -361,6 +390,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_white(msg.white); if (msg.has_color_temperature) call.set_color_temperature(msg.color_temperature); + if (msg.has_cold_white) + call.set_cold_white(msg.cold_white); + if (msg.has_warm_white) + call.set_warm_white(msg.warm_white); if (msg.has_transition_length) call.set_transition_length(msg.transition_length); if (msg.has_flash_length) @@ -395,7 +428,8 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class(); - msg.state_class = static_cast(sensor->state_class); + msg.state_class = static_cast(sensor->get_state_class()); + msg.disabled_by_default = sensor->is_disabled_by_default(); return this->send_list_entities_sensor_response(msg); } @@ -419,6 +453,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { msg.unique_id = get_default_unique_id("switch", a_switch); msg.icon = a_switch->get_icon(); msg.assumed_state = a_switch->assumed_state(); + msg.disabled_by_default = a_switch->is_disabled_by_default(); return this->send_list_entities_switch_response(msg); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { @@ -453,6 +488,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) if (msg.unique_id.empty()) msg.unique_id = get_default_unique_id("text_sensor", text_sensor); msg.icon = text_sensor->get_icon(); + msg.disabled_by_default = text_sensor->is_disabled_by_default(); return this->send_list_entities_text_sensor_response(msg); } #endif @@ -475,14 +511,14 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { } else { resp.target_temperature = climate->target_temperature; } - if (traits.get_supports_away()) - resp.away = climate->away; if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) resp.custom_fan_mode = climate->custom_fan_mode.value(); - if (traits.get_supports_presets() && climate->preset.has_value()) + if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); + resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY; + } if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) @@ -496,42 +532,32 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.object_id = climate->get_object_id(); msg.name = climate->get_name(); msg.unique_id = get_default_unique_id("climate", climate); + + msg.disabled_by_default = climate->is_disabled_by_default(); + msg.icon = climate->get_icon(); + msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); - for (auto mode : - {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_HEAT_COOL}) { - if (traits.supports_mode(mode)) - msg.supported_modes.push_back(static_cast(mode)); - } + + for (auto mode : traits.get_supported_modes()) + msg.supported_modes.push_back(static_cast(mode)); + msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_temperature_step = traits.get_visual_temperature_step(); - msg.supports_away = traits.get_supports_away(); + msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); - for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, - climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { - if (traits.supports_fan_mode(fan_mode)) - msg.supported_fan_modes.push_back(static_cast(fan_mode)); - } - for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) { + + for (auto fan_mode : traits.get_supported_fan_modes()) + msg.supported_fan_modes.push_back(static_cast(fan_mode)); + for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) msg.supported_custom_fan_modes.push_back(custom_fan_mode); - } - for (auto preset : {climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_BOOST, - climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_SLEEP, - climate::CLIMATE_PRESET_ACTIVITY}) { - if (traits.supports_preset(preset)) - msg.supported_presets.push_back(static_cast(preset)); - } - for (auto const &custom_preset : traits.get_supported_custom_presets()) { + for (auto preset : traits.get_supported_presets()) + msg.supported_presets.push_back(static_cast(preset)); + for (auto const &custom_preset : traits.get_supported_custom_presets()) msg.supported_custom_presets.push_back(custom_preset); - } - for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL}) { - if (traits.supports_swing_mode(swing_mode)) - msg.supported_swing_modes.push_back(static_cast(swing_mode)); - } + for (auto swing_mode : traits.get_supported_swing_modes()) + msg.supported_swing_modes.push_back(static_cast(swing_mode)); return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { @@ -548,8 +574,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_away) - call.set_away(msg.away); + if (msg.has_legacy_away) + call.set_preset(msg.legacy_away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) @@ -564,13 +590,86 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { } #endif +#ifdef USE_NUMBER +bool APIConnection::send_number_state(number::Number *number, float state) { + if (!this->state_subscription_) + return false; + + NumberStateResponse resp{}; + resp.key = number->get_object_id_hash(); + resp.state = state; + resp.missing_state = !number->has_state(); + return this->send_number_state_response(resp); +} +bool APIConnection::send_number_info(number::Number *number) { + ListEntitiesNumberResponse msg; + msg.key = number->get_object_id_hash(); + msg.object_id = number->get_object_id(); + msg.name = number->get_name(); + msg.unique_id = get_default_unique_id("number", number); + msg.icon = number->get_icon(); + msg.disabled_by_default = number->is_disabled_by_default(); + + msg.min_value = number->traits.get_min_value(); + msg.max_value = number->traits.get_max_value(); + msg.step = number->traits.get_step(); + + return this->send_list_entities_number_response(msg); +} +void APIConnection::number_command(const NumberCommandRequest &msg) { + number::Number *number = App.get_number_by_key(msg.key); + if (number == nullptr) + return; + + auto call = number->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_) + return false; + + SelectStateResponse resp{}; + resp.key = select->get_object_id_hash(); + resp.state = std::move(state); + resp.missing_state = !select->has_state(); + return this->send_select_state_response(resp); +} +bool APIConnection::send_select_info(select::Select *select) { + ListEntitiesSelectResponse msg; + msg.key = select->get_object_id_hash(); + msg.object_id = select->get_object_id(); + msg.name = select->get_name(); + msg.unique_id = get_default_unique_id("select", select); + msg.icon = select->get_icon(); + msg.disabled_by_default = select->is_disabled_by_default(); + + for (const auto &option : select->traits.get_options()) + msg.options.push_back(option); + + return this->send_list_entities_select_response(msg); +} +void APIConnection::select_command(const SelectCommandRequest &msg) { + select::Select *select = App.get_select_by_key(msg.key); + if (select == nullptr) + return; + + auto call = select->make_call(); + call.set_option(msg.state); + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) return; if (this->image_reader_.available()) return; - this->image_reader_.set_image(image); + this->image_reader_.set_image(std::move(image)); } bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { ListEntitiesCameraResponse msg; @@ -578,6 +677,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { msg.object_id = camera->get_object_id(); msg.name = camera->get_name(); msg.unique_id = get_default_unique_id("camera", camera); + msg.disabled_by_default = camera->is_disabled_by_default(); return this->send_list_entities_camera_response(msg); } void APIConnection::camera_image(const CameraImageRequest &msg) { @@ -606,30 +706,20 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin auto buffer = this->create_buffer(); // LogLevel level = 1; buffer.encode_uint32(1, static_cast(level)); - // string tag = 2; - // buffer.encode_string(2, tag, strlen(tag)); // string message = 3; buffer.encode_string(3, line, strlen(line)); // SubscribeLogsResponse - 29 - bool success = this->send_buffer(buffer, 29); - if (!success) { - buffer = this->create_buffer(); - // bool send_failed = 4; - buffer.encode_bool(4, true); - return this->send_buffer(buffer, 29); - } else { - return true; - } + return this->send_buffer(buffer, 29); } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); - this->client_info_ += ")"; + this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; + this->helper_->set_log_info(client_info_); ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 4; + resp.api_version_minor = 6; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; @@ -641,7 +731,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); this->connection_state_ = ConnectionState::AUTHENTICATED; #ifdef USE_HOMEASSISTANT_TIME @@ -659,9 +749,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); -#ifdef ARDUINO_BOARD - resp.model = ARDUINO_BOARD; -#endif + resp.model = ESPHOME_BOARD; #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif @@ -689,30 +777,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { - for (auto &it : this->parent_->get_state_subs()) { - SubscribeHomeAssistantStateResponse resp; - resp.entity_id = it.entity_id; - resp.attribute = it.attribute.value(); - if (!this->send_subscribe_home_assistant_state_response(resp)) { - this->on_fatal_error(); - return; - } - } + state_subs_at_ = 0; } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { if (this->remove_) return false; - - std::vector header; - header.push_back(0x00); - ProtoVarInt(buffer.get_buffer()->size()).encode(header); - ProtoVarInt(message_type).encode(header); - - size_t needed_space = buffer.get_buffer()->size() + header.size(); - - if (needed_space > this->client_->space()) { + if (!this->helper_->can_write_without_blocking()) { delay(0); - if (needed_space > this->client_->space()) { + APIError err = 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); + return false; + } + if (!this->helper_->can_write_without_blocking()) { // SubscribeLogsResponse if (message_type != 29) { ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); @@ -722,24 +800,31 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } } - this->client_->add(reinterpret_cast(header.data()), header.size(), - ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); - this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size(), - ASYNC_WRITE_FLAG_COPY); - bool ret = this->client_->send(); - return ret; + APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); + if (err == APIError::WOULD_BLOCK) + return false; + 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()); + } else { + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + } + return false; + } + this->last_traffic_ = millis(); + return true; } void APIConnection::on_unauthenticated_access() { - ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); } void APIConnection::on_no_setup_connection() { - ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); } void APIConnection::on_fatal_error() { - ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); - this->client_->close(); + this->helper_->close(); this->remove_ = true; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3e91ead52c..a1f1769a19 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -5,16 +5,17 @@ #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" +#include "api_frame_helper.h" namespace esphome { namespace api { class APIConnection : public APIServerConnection { public: - APIConnection(AsyncClient *client, APIServer *parent); - virtual ~APIConnection(); + APIConnection(std::unique_ptr socket, APIServer *parent); + virtual ~APIConnection() = default; - void disconnect_client(); + void start(); void loop(); bool send_list_info_done() { @@ -62,6 +63,16 @@ class APIConnection : public APIServerConnection { bool send_climate_state(climate::Climate *climate); bool send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; +#endif +#ifdef USE_NUMBER + bool send_number_state(number::Number *number, float state); + bool send_number_info(number::Number *number); + void number_command(const NumberCommandRequest &msg) override; +#endif +#ifdef USE_SELECT + bool send_select_state(select::Select *select, std::string state); + bool send_select_info(select::Select *select); + void select_command(const SelectCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { @@ -76,10 +87,7 @@ class APIConnection : public APIServerConnection { } #endif - void on_disconnect_response(const DisconnectResponse &value) override { - // we initiated disconnect_client - this->next_close_ = true; - } + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping this->sent_ping_ = false; @@ -90,12 +98,7 @@ class APIConnection : public APIServerConnection { #endif HelloResponse hello(const HelloRequest &msg) override; ConnectResponse connect(const ConnectRequest &msg) override; - DisconnectResponse disconnect(const DisconnectRequest &msg) override { - // remote initiated disconnect_client - this->next_close_ = true; - DisconnectResponse resp; - return resp; - } + DisconnectResponse disconnect(const DisconnectRequest &msg) override; PingResponse ping(const PingRequest &msg) override { return {}; } DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } @@ -125,19 +128,16 @@ class APIConnection : public APIServerConnection { void on_unauthenticated_access() override; void on_no_setup_connection() override; ProtoWriteBuffer create_buffer() override { - this->send_buffer_.clear(); - return {&this->send_buffer_}; + // FIXME: ensure no recursive writes can happen + this->proto_write_buffer_.clear(); + return {&this->proto_write_buffer_}; } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; protected: friend APIServer; - void on_error_(int8_t error); - void on_disconnect_(); - void on_timeout_(uint32_t time); - void on_data_(uint8_t *buf, size_t len); - void parse_recv_buffer_(); + bool send_(const void *buf, size_t len, bool force); enum class ConnectionState { WAITING_FOR_HELLO, @@ -147,8 +147,10 @@ class APIConnection : public APIServerConnection { bool remove_{false}; - std::vector send_buffer_; - std::vector recv_buffer_; + // Buffer used to encode proto messages + // Re-use to prevent allocations + std::vector proto_write_buffer_; + std::unique_ptr helper_; std::string client_info_; #ifdef USE_ESP32_CAMERA @@ -160,12 +162,11 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; - bool current_nodelay_{false}; - bool next_close_{false}; - AsyncClient *client_; + bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; + int state_subs_at_ = -1; }; } // namespace api diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp new file mode 100644 index 0000000000..4971272f41 --- /dev/null +++ b/esphome/components/api/api_frame_helper.cpp @@ -0,0 +1,998 @@ +#include "api_frame_helper.h" + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "proto.h" +#include + +namespace esphome { +namespace api { + +static const char *const TAG = "api.socket"; + +/// Is the given return value (from read/write syscalls) a wouldblock error? +bool is_would_block(ssize_t ret) { + if (ret == -1) { + return errno == EWOULDBLOCK || errno == EAGAIN; + } + return ret == 0; +} + +const char *api_error_to_str(APIError err) { + // not using switch to ensure compiler doesn't try to build a big table out of it + if (err == APIError::OK) { + return "OK"; + } else if (err == APIError::WOULD_BLOCK) { + return "WOULD_BLOCK"; + } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { + return "BAD_HANDSHAKE_PACKET_LEN"; + } else if (err == APIError::BAD_INDICATOR) { + return "BAD_INDICATOR"; + } else if (err == APIError::BAD_DATA_PACKET) { + return "BAD_DATA_PACKET"; + } else if (err == APIError::TCP_NODELAY_FAILED) { + return "TCP_NODELAY_FAILED"; + } else if (err == APIError::TCP_NONBLOCKING_FAILED) { + return "TCP_NONBLOCKING_FAILED"; + } else if (err == APIError::CLOSE_FAILED) { + return "CLOSE_FAILED"; + } else if (err == APIError::SHUTDOWN_FAILED) { + return "SHUTDOWN_FAILED"; + } else if (err == APIError::BAD_STATE) { + return "BAD_STATE"; + } else if (err == APIError::BAD_ARG) { + return "BAD_ARG"; + } else if (err == APIError::SOCKET_READ_FAILED) { + return "SOCKET_READ_FAILED"; + } else if (err == APIError::SOCKET_WRITE_FAILED) { + return "SOCKET_WRITE_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { + return "HANDSHAKESTATE_READ_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { + return "HANDSHAKESTATE_WRITE_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { + return "HANDSHAKESTATE_BAD_STATE"; + } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { + return "CIPHERSTATE_DECRYPT_FAILED"; + } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { + return "CIPHERSTATE_ENCRYPT_FAILED"; + } else if (err == APIError::OUT_OF_MEMORY) { + return "OUT_OF_MEMORY"; + } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { + return "HANDSHAKESTATE_SETUP_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { + return "HANDSHAKESTATE_SPLIT_FAILED"; + } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { + return "BAD_HANDSHAKE_ERROR_BYTE"; + } + return "UNKNOWN"; +} + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) +// uncomment to log raw packets +//#define HELPER_LOG_PACKETS + +#ifdef USE_API_NOISE +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; + +/// Convert a noise error code to a readable error +std::string noise_err_to_str(int err) { + if (err == NOISE_ERROR_NO_MEMORY) + return "NO_MEMORY"; + if (err == NOISE_ERROR_UNKNOWN_ID) + return "UNKNOWN_ID"; + if (err == NOISE_ERROR_UNKNOWN_NAME) + return "UNKNOWN_NAME"; + if (err == NOISE_ERROR_MAC_FAILURE) + return "MAC_FAILURE"; + if (err == NOISE_ERROR_NOT_APPLICABLE) + return "NOT_APPLICABLE"; + if (err == NOISE_ERROR_SYSTEM) + return "SYSTEM"; + if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) + return "REMOTE_KEY_REQUIRED"; + if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) + return "LOCAL_KEY_REQUIRED"; + if (err == NOISE_ERROR_PSK_REQUIRED) + return "PSK_REQUIRED"; + if (err == NOISE_ERROR_INVALID_LENGTH) + return "INVALID_LENGTH"; + if (err == NOISE_ERROR_INVALID_PARAM) + return "INVALID_PARAM"; + if (err == NOISE_ERROR_INVALID_STATE) + return "INVALID_STATE"; + if (err == NOISE_ERROR_INVALID_NONCE) + return "INVALID_NONCE"; + if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) + return "INVALID_PRIVATE_KEY"; + if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) + return "INVALID_PUBLIC_KEY"; + if (err == NOISE_ERROR_INVALID_FORMAT) + return "INVALID_FORMAT"; + if (err == NOISE_ERROR_INVALID_SIGNATURE) + return "INVALID_SIGNATURE"; + return to_string(err); +} + +/// Initialize the frame helper, returns OK if successful. +APIError APINoiseFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + // init prologue + prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + + state_ = State::CLIENT_HELLO; + return APIError::OK; +} +/// Run through handshake messages (if in that phase) +APIError APINoiseFrameHelper::loop() { + APIError err = state_action_(); + if (err == APIError::WOULD_BLOCK) + return APIError::OK; + if (err != APIError::OK) + return err; + if (!tx_buf_.empty()) { + err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg_start: points to the start of the payload - this pointer is only valid until the next + * try_receive_raw_ call + * + * @return 0 if a full packet is in rx_buf_ + * @return -1 if error, check errno. + * + * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. + * errno ENOMEM: Not enough memory for reading packet. + * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. + */ +APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + if (rx_header_buf_len_ < 3) { + // no header information yet + size_t to_read = 3 - rx_header_buf_len_; + ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_len_ += received; + if (received != to_read) { + // not a full read + return APIError::WOULD_BLOCK; + } + + // header reading done + } + + // read body + uint8_t indicator = rx_header_buf_[0]; + if (indicator != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", indicator); + return APIError::BAD_INDICATOR; + } + + uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; + + if (state_ != State::DATA && msg_size > 128) { + // for handshake message only permit up to 128 bytes + state_ = State::FAILED; + HELPER_LOG("Bad packet len for handshake: %d", msg_size); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + // reserve space for body + if (rx_buf_.size() != msg_size) { + rx_buf_.resize(msg_size); + } + + if (rx_buf_len_ < msg_size) { + // more data to read + size_t to_read = msg_size - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#endif + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_len_ = 0; + return APIError::OK; +} + +/** To be called from read/write methods. + * + * This method runs through the internal handshake methods, if in that state. + * + * If the handshake is still active when this method returns and a read/write can't take place at + * the moment, returns WOULD_BLOCK. + * If an error occured, returns that error. Only returns OK if the transport is ready for data + * traffic. + */ +APIError APINoiseFrameHelper::state_action_() { + int err; + APIError aerr; + if (state_ == State::INITIALIZE) { + HELPER_LOG("Bad state for method: %d", (int) state_); + return APIError::BAD_STATE; + } + if (state_ == State::CLIENT_HELLO) { + // waiting for client hello + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + return aerr; + } + if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + return aerr; + } + if (aerr != APIError::OK) + return aerr; + // ignore contents, may be used in future for flags + prologue_.push_back((uint8_t)(frame.msg.size() >> 8)); + prologue_.push_back((uint8_t) frame.msg.size()); + prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); + + state_ = State::SERVER_HELLO; + } + if (state_ == State::SERVER_HELLO) { + // send server hello + uint8_t msg[1]; + msg[0] = 0x01; // chosen proto + aerr = write_frame_(msg, 1); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + } + if (state_ == State::HANDSHAKE) { + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + // waiting for handshake msg + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + return aerr; + } + if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + return aerr; + } + if (aerr != APIError::OK) + return aerr; + + if (frame.msg.empty()) { + send_explicit_handshake_reject_("Empty handshake message"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } else if (frame.msg[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); + send_explicit_handshake_reject_("Bad handshake error byte"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); + err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); + if (err == NOISE_ERROR_MAC_FAILURE) { + send_explicit_handshake_reject_("Handshake MAC failure"); + } else { + send_explicit_handshake_reject_("Handshake error"); + } + return APIError::HANDSHAKESTATE_READ_FAILED; + } + + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_WRITE_FAILED; + } + buffer[0] = 0x00; // success + + aerr = write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else { + // bad state for action + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + } + if (state_ == State::CLOSED || state_ == State::FAILED) { + return APIError::BAD_STATE; + } + return APIError::OK; +} +void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { + std::vector data; + data.resize(reason.length() + 1); + data[0] = 0x01; // failure + for (size_t i = 0; i < reason.length(); i++) { + data[i + 1] = (uint8_t) reason[i]; + } + // temporarily remove failed state + auto orig_state = state_; + state_ = State::EXPLICIT_REJECT; + write_frame_(data.data(), data.size()); + state_ = orig_state; +} + +APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); + err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_DECRYPT_FAILED; + } + + size_t msg_size = mbuf.size; + uint8_t *msg_data = frame.msg.data(); + if (msg_size < 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: size %d too short", msg_size); + return APIError::BAD_DATA_PACKET; + } + + // uint16_t type; + // uint16_t data_len; + // uint8_t *data; + // uint8_t *padding; zero or more bytes to fill up the rest of the packet + uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; + if (data_len > msg_size - 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); + return APIError::BAD_DATA_PACKET; + } + + buffer->container = std::move(frame.msg); + buffer->data_offset = 4; + buffer->data_len = data_len; + buffer->type = type; + return APIError::OK; +} +bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + size_t padding = 0; + size_t msg_len = 4 + payload_len + padding; + size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); + auto tmpbuf = std::unique_ptr{new (std::nothrow) uint8_t[frame_len]}; + if (tmpbuf == nullptr) { + HELPER_LOG("Could not allocate for writing packet"); + return APIError::OUT_OF_MEMORY; + } + + tmpbuf[0] = 0x01; // indicator + // tmpbuf[1], tmpbuf[2] to be set later + const uint8_t msg_offset = 3; + const uint8_t payload_offset = msg_offset + 4; + tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8); // type + tmpbuf[msg_offset + 1] = (uint8_t) type; + tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8); // data_len + tmpbuf[msg_offset + 3] = (uint8_t) payload_len; + // copy data + std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); + // fill padding with zeros + std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); + err = noise_cipherstate_encrypt(send_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_ENCRYPT_FAILED; + } + + size_t total_len = 3 + mbuf.size; + tmpbuf[1] = (uint8_t)(mbuf.size >> 8); + tmpbuf[2] = (uint8_t) mbuf.size; + + struct iovec iov; + iov.iov_base = &tmpbuf[0]; + iov.iov_len = total_len; + + // write raw to not have two packets sent if NAGLE disabled + return write_raw_(&iov, 1); +} +APIError APINoiseFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (sent == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent == 0) { + break; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { + if (iovcnt == 0) + return APIError::OK; + int err; + APIError aerr; + + size_t total_write_len = 0; + for (int i = 0; i < iovcnt; i++) { +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); +#endif + total_write_len += iov[i].iov_len; + } + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + for (int i = 0; i < iovcnt; i++) { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; + } + + ssize_t sent = socket_->writev(iov, iovcnt); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + for (int i = 0; i < iovcnt; i++) { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != total_write_len) { + // partially sent, add end to tx_buf + size_t to_consume = sent; + for (int i = 0; i < iovcnt; i++) { + if (to_consume >= iov[i].iov_len) { + to_consume -= iov[i].iov_len; + } else { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base) + to_consume, + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + to_consume = 0; + } + } + return APIError::OK; + } + // fully sent + return APIError::OK; +} +APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t)(len >> 8); + header[2] = (uint8_t) len; + + struct iovec iov[2]; + iov[0].iov_base = header; + iov[0].iov_len = 3; + iov[1].iov_base = const_cast(data); + iov[1].iov_len = len; + + return write_raw_(iov, 2); +} + +/** Initiate the data structures for the handshake. + * + * @return 0 on success, -1 on error (check errno) + */ +APIError APINoiseFrameHelper::init_handshake_() { + int err; + memset(&nid_, 0, sizeof(nid_)); + // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); + nid_.pattern_id = NOISE_PATTERN_NN; + nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid_.dh_id = NOISE_DH_CURVE25519; + nid_.prefix_id = NOISE_PREFIX_STANDARD; + nid_.hybrid_id = NOISE_DH_NONE; + nid_.hash_id = NOISE_HASH_SHA256; + nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + const auto &psk = ctx_->get_psk(); + err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + // set_prologue copies it into handshakestate, so we can get rid of it now + prologue_ = {}; + + err = noise_handshakestate_start(handshake_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + return APIError::OK; +} + +APIError APINoiseFrameHelper::check_handshake_finished_() { + assert(state_ == State::HANDSHAKE); + + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + return APIError::OK; + if (action != NOISE_ACTION_SPLIT) { + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SPLIT_FAILED; + } + + HELPER_LOG("Handshake complete!"); + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + state_ = State::DATA; + return APIError::OK; +} + +APINoiseFrameHelper::~APINoiseFrameHelper() { + if (handshake_ != nullptr) { + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + } + if (send_cipher_ != nullptr) { + noise_cipherstate_free(send_cipher_); + send_cipher_ = nullptr; + } + if (recv_cipher_ != nullptr) { + noise_cipherstate_free(recv_cipher_); + recv_cipher_ = nullptr; + } +} + +APIError APINoiseFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APINoiseFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} +extern "C" { +// declare how noise generates random bytes (here with a good HWRNG based on the RF system) +void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast(output), len); } +} +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT + +/// Initialize the frame helper, returns OK if successful. +APIError APIPlaintextFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + state_ = State::DATA; + return APIError::OK; +} +/// Not used for plaintext +APIError APIPlaintextFrameHelper::loop() { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + // try send pending TX data + if (!tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg: store the parsed frame in that struct + * + * @return See APIError + * + * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + */ +APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + while (!rx_header_parsed_) { + uint8_t data; + ssize_t received = socket_->read(&data, 1); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_.push_back(data); + + // try parse header + if (rx_header_buf_[0] != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + + size_t i = 1; + uint32_t consumed = 0; + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_size_varint.has_value()) { + // not enough data there yet + continue; + } + + i += consumed; + rx_header_parsed_len_ = msg_size_varint->as_uint32(); + + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_type_varint.has_value()) { + // not enough data there yet + continue; + } + rx_header_parsed_type_ = msg_type_varint->as_uint32(); + rx_header_parsed_ = true; + } + // header reading done + + // reserve space for body + if (rx_buf_.size() != rx_header_parsed_len_) { + rx_buf_.resize(rx_header_parsed_len_); + } + + if (rx_buf_len_ < rx_header_parsed_len_) { + // more data to read + size_t to_read = rx_header_parsed_len_ - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#endif + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_.clear(); + rx_header_parsed_ = false; + return APIError::OK; +} + +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + buffer->container = std::move(frame.msg); + buffer->data_offset = 0; + buffer->data_len = rx_header_parsed_len_; + buffer->type = rx_header_parsed_type_; + return APIError::OK; +} +bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + + std::vector header; + header.push_back(0x00); + ProtoVarInt(payload_len).encode(header); + ProtoVarInt(type).encode(header); + + struct iovec iov[2]; + iov[0].iov_base = &header[0]; + iov[0].iov_len = header.size(); + iov[1].iov_base = const_cast(payload); + iov[1].iov_len = payload_len; + + return write_raw_(iov, 2); +} +APIError APIPlaintextFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (is_would_block(sent)) { + break; + } else if (sent == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { + if (iovcnt == 0) + return APIError::OK; + int err; + APIError aerr; + + size_t total_write_len = 0; + for (int i = 0; i < iovcnt; i++) { +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); +#endif + total_write_len += iov[i].iov_len; + } + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + for (int i = 0; i < iovcnt; i++) { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; + } + + ssize_t sent = socket_->writev(iov, iovcnt); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + for (int i = 0; i < iovcnt; i++) { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base), + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + } + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != total_write_len) { + // partially sent, add end to tx_buf + size_t to_consume = sent; + for (int i = 0; i < iovcnt; i++) { + if (to_consume >= iov[i].iov_len) { + to_consume -= iov[i].iov_len; + } else { + tx_buf_.insert(tx_buf_.end(), reinterpret_cast(iov[i].iov_base) + to_consume, + reinterpret_cast(iov[i].iov_base) + iov[i].iov_len); + to_consume = 0; + } + } + return APIError::OK; + } + // fully sent + return APIError::OK; +} + +APIError APIPlaintextFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} +#endif // USE_API_PLAINTEXT + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h new file mode 100644 index 0000000000..7fdb26fd40 --- /dev/null +++ b/esphome/components/api/api_frame_helper.h @@ -0,0 +1,184 @@ +#pragma once +#include +#include +#include +#include + +#include "esphome/core/defines.h" + +#ifdef USE_API_NOISE +#include "noise/protocol.h" +#endif + +#include "esphome/components/socket/socket.h" +#include "api_noise_context.h" + +namespace esphome { +namespace api { + +struct ReadPacketBuffer { + std::vector container; + uint16_t type; + size_t data_offset; + size_t data_len; +}; + +struct PacketBuffer { + const std::vector container; + uint16_t type; + uint8_t data_offset; + uint8_t data_len; +}; + +enum class APIError : int { + OK = 0, + WOULD_BLOCK = 1001, + BAD_HANDSHAKE_PACKET_LEN = 1002, + BAD_INDICATOR = 1003, + BAD_DATA_PACKET = 1004, + TCP_NODELAY_FAILED = 1005, + TCP_NONBLOCKING_FAILED = 1006, + CLOSE_FAILED = 1007, + SHUTDOWN_FAILED = 1008, + BAD_STATE = 1009, + BAD_ARG = 1010, + SOCKET_READ_FAILED = 1011, + SOCKET_WRITE_FAILED = 1012, + HANDSHAKESTATE_READ_FAILED = 1013, + HANDSHAKESTATE_WRITE_FAILED = 1014, + HANDSHAKESTATE_BAD_STATE = 1015, + CIPHERSTATE_DECRYPT_FAILED = 1016, + CIPHERSTATE_ENCRYPT_FAILED = 1017, + OUT_OF_MEMORY = 1018, + HANDSHAKESTATE_SETUP_FAILED = 1019, + HANDSHAKESTATE_SPLIT_FAILED = 1020, + BAD_HANDSHAKE_ERROR_BYTE = 1021, +}; + +const char *api_error_to_str(APIError err); + +class APIFrameHelper { + public: + virtual ~APIFrameHelper() = default; + virtual APIError init() = 0; + virtual APIError loop() = 0; + virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; + virtual bool can_write_without_blocking() = 0; + virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; + virtual std::string getpeername() = 0; + virtual APIError close() = 0; + virtual APIError shutdown(int how) = 0; + // Give this helper a name for logging + virtual void set_log_info(std::string info) = 0; +}; + +#ifdef USE_API_NOISE +class APINoiseFrameHelper : public APIFrameHelper { + public: + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) + : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} + ~APINoiseFrameHelper() override; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError state_action_(); + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_frame_(const uint8_t *data, size_t len); + APIError write_raw_(const struct iovec *iov, int iovcnt); + APIError init_handshake_(); + APIError check_handshake_finished_(); + void send_explicit_handshake_reject_(const std::string &reason); + + std::unique_ptr socket_; + + std::string info_; + uint8_t rx_header_buf_[3]; + size_t rx_header_buf_len_ = 0; + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + std::vector prologue_; + + std::shared_ptr ctx_; + NoiseHandshakeState *handshake_ = nullptr; + NoiseCipherState *send_cipher_ = nullptr; + NoiseCipherState *recv_cipher_ = nullptr; + NoiseProtocolId nid_; + + enum class State { + INITIALIZE = 1, + CLIENT_HELLO = 2, + SERVER_HELLO = 3, + HANDSHAKE = 4, + DATA = 5, + CLOSED = 6, + FAILED = 7, + EXPLICIT_REJECT = 8, + } state_ = State::INITIALIZE; +}; +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT +class APIPlaintextFrameHelper : public APIFrameHelper { + public: + APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + ~APIPlaintextFrameHelper() override = default; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_raw_(const struct iovec *iov, int iovcnt); + + std::unique_ptr socket_; + + std::string info_; + std::vector rx_header_buf_; + bool rx_header_parsed_ = false; + uint32_t rx_header_parsed_type_ = 0; + uint32_t rx_header_parsed_len_ = 0; + + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + + enum class State { + INITIALIZE = 1, + DATA = 2, + CLOSED = 3, + FAILED = 4, + } state_ = State::INITIALIZE; +}; +#endif + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h new file mode 100644 index 0000000000..324e69d945 --- /dev/null +++ b/esphome/components/api/api_noise_context.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +#ifdef USE_API_NOISE +using psk_t = std::array; + +class APINoiseContext { + public: + void set_psk(psk_t psk) { psk_ = psk; } + const psk_t &get_psk() const { return psk_; } + + protected: + psk_t psk_; +}; +#endif // USE_API_NOISE + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index a9e9d64bc1..6a87238186 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -62,12 +62,52 @@ template<> const char *proto_enum_to_string(enums::FanDirec return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::ColorMode value) { + switch (value) { + case enums::COLOR_MODE_UNKNOWN: + return "COLOR_MODE_UNKNOWN"; + case enums::COLOR_MODE_ON_OFF: + return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_BRIGHTNESS: + return "COLOR_MODE_BRIGHTNESS"; + case enums::COLOR_MODE_WHITE: + return "COLOR_MODE_WHITE"; + case enums::COLOR_MODE_COLOR_TEMPERATURE: + return "COLOR_MODE_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_COLD_WARM_WHITE: + return "COLOR_MODE_COLD_WARM_WHITE"; + case enums::COLOR_MODE_RGB: + return "COLOR_MODE_RGB"; + case enums::COLOR_MODE_RGB_WHITE: + return "COLOR_MODE_RGB_WHITE"; + case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: + return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: + return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: return "STATE_CLASS_NONE"; case enums::STATE_CLASS_MEASUREMENT: return "STATE_CLASS_MEASUREMENT"; + case enums::STATE_CLASS_TOTAL_INCREASING: + return "STATE_CLASS_TOTAL_INCREASING"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { + switch (value) { + case enums::LAST_RESET_NONE: + return "LAST_RESET_NONE"; + case enums::LAST_RESET_NEVER: + return "LAST_RESET_NEVER"; + case enums::LAST_RESET_AUTO: + return "LAST_RESET_AUTO"; default: return "UNKNOWN"; } @@ -82,6 +122,8 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "LOG_LEVEL_WARN"; case enums::LOG_LEVEL_INFO: return "LOG_LEVEL_INFO"; + case enums::LOG_LEVEL_CONFIG: + return "LOG_LEVEL_CONFIG"; case enums::LOG_LEVEL_DEBUG: return "LOG_LEVEL_DEBUG"; case enums::LOG_LEVEL_VERBOSE: @@ -192,16 +234,18 @@ template<> const char *proto_enum_to_string(enums::Climate } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { - case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + case enums::CLIMATE_PRESET_NONE: + return "CLIMATE_PRESET_NONE"; + case enums::CLIMATE_PRESET_HOME: + return "CLIMATE_PRESET_HOME"; case enums::CLIMATE_PRESET_AWAY: return "CLIMATE_PRESET_AWAY"; case enums::CLIMATE_PRESET_BOOST: return "CLIMATE_PRESET_BOOST"; case enums::CLIMATE_PRESET_COMFORT: return "CLIMATE_PRESET_COMFORT"; - case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + case enums::CLIMATE_PRESET_ECO: + return "CLIMATE_PRESET_ECO"; case enums::CLIMATE_PRESET_SLEEP: return "CLIMATE_PRESET_SLEEP"; case enums::CLIMATE_PRESET_ACTIVITY: @@ -221,6 +265,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } } void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HelloRequest::dump_to(std::string &out) const { char buffer[64]; out.append("HelloRequest {\n"); @@ -229,6 +274,7 @@ void HelloRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -258,6 +304,7 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HelloResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HelloResponse {\n"); @@ -276,6 +323,7 @@ void HelloResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -287,6 +335,7 @@ bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value } } void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ConnectRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ConnectRequest {\n"); @@ -295,6 +344,7 @@ void ConnectRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -306,6 +356,7 @@ bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { } } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ConnectResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ConnectResponse {\n"); @@ -314,16 +365,27 @@ void ConnectResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } +#endif void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } +#endif void PingRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } +#endif void PingResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } +#endif void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +#endif bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -383,6 +445,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); } +#ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { char buffer[64]; out.append("DeviceInfoResponse {\n"); @@ -423,18 +486,29 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } +#endif void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } +#endif void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } +#endif bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { this->is_status_binary_sensor = value.as_bool(); return true; } + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -457,6 +531,10 @@ bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLen this->device_class = value.as_string(); return true; } + case 8: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -478,7 +556,10 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); + buffer.encode_bool(7, this->disabled_by_default); + buffer.encode_string(8, this->icon); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesBinarySensorResponse {\n"); @@ -506,8 +587,17 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(" is_status_binary_sensor: "); out.append(YESNO(this->is_status_binary_sensor)); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); out.append("}"); } +#endif bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -537,6 +627,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void BinarySensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("BinarySensorStateResponse {\n"); @@ -554,6 +645,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -568,6 +660,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_tilt = value.as_bool(); return true; } + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -590,6 +686,10 @@ bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->device_class = value.as_string(); return true; } + case 10: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -613,7 +713,10 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); buffer.encode_string(8, this->device_class); + buffer.encode_bool(9, this->disabled_by_default); + buffer.encode_string(10, this->icon); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesCoverResponse {\n"); @@ -649,8 +752,17 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); out.append("}"); } +#endif bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -690,6 +802,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->tilt); buffer.encode_enum(5, this->current_operation); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CoverStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("CoverStateResponse {\n"); @@ -717,6 +830,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -771,6 +885,7 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->tilt); buffer.encode_bool(8, this->stop); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CoverCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("CoverCommandRequest {\n"); @@ -810,6 +925,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -828,6 +944,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supported_speed_count = value.as_int32(); return true; } + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -846,6 +966,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->unique_id = value.as_string(); return true; } + case 10: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -869,7 +993,10 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); buffer.encode_int32(8, this->supported_speed_count); + buffer.encode_bool(9, this->disabled_by_default); + buffer.encode_string(10, this->icon); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesFanResponse {\n"); @@ -906,8 +1033,17 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { sprintf(buffer, "%d", this->supported_speed_count); out.append(buffer); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); out.append("}"); } +#endif bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -952,6 +1088,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); } +#ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("FanStateResponse {\n"); @@ -982,6 +1119,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1051,6 +1189,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); } +#ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("FanCommandRequest {\n"); @@ -1101,22 +1240,31 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { + case 12: { + this->supported_color_modes.push_back(value.as_enum()); + return true; + } case 5: { - this->supports_brightness = value.as_bool(); + this->legacy_supports_brightness = value.as_bool(); return true; } case 6: { - this->supports_rgb = value.as_bool(); + this->legacy_supports_rgb = value.as_bool(); return true; } case 7: { - this->supports_white_value = value.as_bool(); + this->legacy_supports_white_value = value.as_bool(); return true; } case 8: { - this->supports_color_temperature = value.as_bool(); + this->legacy_supports_color_temperature = value.as_bool(); + return true; + } + case 13: { + this->disabled_by_default = value.as_bool(); return true; } default: @@ -1141,6 +1289,10 @@ bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->effects.push_back(value.as_string()); return true; } + case 14: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -1168,16 +1320,22 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); - buffer.encode_bool(5, this->supports_brightness); - buffer.encode_bool(6, this->supports_rgb); - buffer.encode_bool(7, this->supports_white_value); - buffer.encode_bool(8, this->supports_color_temperature); + for (auto &it : this->supported_color_modes) { + buffer.encode_enum(12, it, true); + } + buffer.encode_bool(5, this->legacy_supports_brightness); + buffer.encode_bool(6, this->legacy_supports_rgb); + buffer.encode_bool(7, this->legacy_supports_white_value); + buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { buffer.encode_string(11, it, true); } + buffer.encode_bool(13, this->disabled_by_default); + buffer.encode_string(14, this->icon); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesLightResponse {\n"); @@ -1198,20 +1356,26 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(this->unique_id).append("'"); out.append("\n"); - out.append(" supports_brightness: "); - out.append(YESNO(this->supports_brightness)); + for (const auto &it : this->supported_color_modes) { + out.append(" supported_color_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); out.append("\n"); - out.append(" supports_rgb: "); - out.append(YESNO(this->supports_rgb)); + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); out.append("\n"); - out.append(" supports_white_value: "); - out.append(YESNO(this->supports_white_value)); + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); out.append("\n"); - out.append(" supports_color_temperature: "); - out.append(YESNO(this->supports_color_temperature)); + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); out.append("\n"); out.append(" min_mireds: "); @@ -1229,14 +1393,27 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); out.append("}"); } +#endif bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_bool(); return true; } + case 11: { + this->color_mode = value.as_enum(); + return true; + } default: return false; } @@ -1261,6 +1438,10 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 10: { + this->color_brightness = value.as_float(); + return true; + } case 4: { this->red = value.as_float(); return true; @@ -1281,6 +1462,14 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 12: { + this->cold_white = value.as_float(); + return true; + } + case 13: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1289,13 +1478,18 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); + buffer.encode_enum(11, this->color_mode); + buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); buffer.encode_float(6, this->blue); buffer.encode_float(7, this->white); buffer.encode_float(8, this->color_temperature); + buffer.encode_float(12, this->cold_white); + buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); } +#ifdef HAS_PROTO_MESSAGE_DUMP void LightStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("LightStateResponse {\n"); @@ -1313,6 +1507,15 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" red: "); sprintf(buffer, "%g", this->red); out.append(buffer); @@ -1338,11 +1541,22 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" effect: "); out.append("'").append(this->effect).append("'"); out.append("\n"); out.append("}"); } +#endif bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1357,6 +1571,18 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 22: { + this->has_color_mode = value.as_bool(); + return true; + } + case 23: { + this->color_mode = value.as_enum(); + return true; + } + case 20: { + this->has_color_brightness = value.as_bool(); + return true; + } case 6: { this->has_rgb = value.as_bool(); return true; @@ -1369,6 +1595,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_color_temperature = value.as_bool(); return true; } + case 24: { + this->has_cold_white = value.as_bool(); + return true; + } + case 26: { + this->has_warm_white = value.as_bool(); + return true; + } case 14: { this->has_transition_length = value.as_bool(); return true; @@ -1413,6 +1647,10 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 21: { + this->color_brightness = value.as_float(); + return true; + } case 7: { this->red = value.as_float(); return true; @@ -1433,6 +1671,14 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 25: { + this->cold_white = value.as_float(); + return true; + } + case 27: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1443,6 +1689,10 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_brightness); buffer.encode_float(5, this->brightness); + buffer.encode_bool(22, this->has_color_mode); + buffer.encode_enum(23, this->color_mode); + buffer.encode_bool(20, this->has_color_brightness); + buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); buffer.encode_float(7, this->red); buffer.encode_float(8, this->green); @@ -1451,6 +1701,10 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(11, this->white); buffer.encode_bool(12, this->has_color_temperature); buffer.encode_float(13, this->color_temperature); + buffer.encode_bool(24, this->has_cold_white); + buffer.encode_float(25, this->cold_white); + buffer.encode_bool(26, this->has_warm_white); + buffer.encode_float(27, this->warm_white); buffer.encode_bool(14, this->has_transition_length); buffer.encode_uint32(15, this->transition_length); buffer.encode_bool(16, this->has_flash_length); @@ -1458,6 +1712,7 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(18, this->has_effect); buffer.encode_string(19, this->effect); } +#ifdef HAS_PROTO_MESSAGE_DUMP void LightCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("LightCommandRequest {\n"); @@ -1483,6 +1738,23 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_mode: "); + out.append(YESNO(this->has_color_mode)); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + + out.append(" has_color_brightness: "); + out.append(YESNO(this->has_color_brightness)); + out.append("\n"); + + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" has_rgb: "); out.append(YESNO(this->has_rgb)); out.append("\n"); @@ -1520,6 +1792,24 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_cold_white: "); + out.append(YESNO(this->has_cold_white)); + out.append("\n"); + + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_warm_white: "); + out.append(YESNO(this->has_warm_white)); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" has_transition_length: "); out.append(YESNO(this->has_transition_length)); out.append("\n"); @@ -1547,6 +1837,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 7: { @@ -1561,6 +1852,14 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->state_class = value.as_enum(); return true; } + case 11: { + this->legacy_last_reset_type = value.as_enum(); + return true; + } + case 12: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -1616,7 +1915,10 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->force_update); buffer.encode_string(9, this->device_class); buffer.encode_enum(10, this->state_class); + buffer.encode_enum(11, this->legacy_last_reset_type); + buffer.encode_bool(12, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesSensorResponse {\n"); @@ -1661,8 +1963,17 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" state_class: "); out.append(proto_enum_to_string(this->state_class)); out.append("\n"); + + out.append(" legacy_last_reset_type: "); + out.append(proto_enum_to_string(this->legacy_last_reset_type)); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -1692,6 +2003,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SensorStateResponse {\n"); @@ -1710,12 +2022,17 @@ void SensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { this->assumed_state = value.as_bool(); return true; } + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -1759,7 +2076,9 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->assumed_state); + buffer.encode_bool(7, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesSwitchResponse {\n"); @@ -1787,8 +2106,13 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" assumed_state: "); out.append(YESNO(this->assumed_state)); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1813,6 +2137,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SwitchStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SwitchStateResponse {\n"); @@ -1826,6 +2151,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1850,6 +2176,7 @@ void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SwitchCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("SwitchCommandRequest {\n"); @@ -1863,6 +2190,17 @@ void SwitchCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -1901,7 +2239,9 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { 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); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesTextSensorResponse {\n"); @@ -1925,8 +2265,13 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { 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("}"); } +#endif bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -1962,6 +2307,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void TextSensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("TextSensorStateResponse {\n"); @@ -1979,6 +2325,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -1997,6 +2344,7 @@ void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); buffer.encode_bool(2, this->dump_config); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsRequest::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsRequest {\n"); @@ -2009,6 +2357,7 @@ void SubscribeLogsRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2025,10 +2374,6 @@ bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) } bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { - this->tag = value.as_string(); - return true; - } case 3: { this->message = value.as_string(); return true; @@ -2039,10 +2384,10 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); - buffer.encode_string(2, this->tag); buffer.encode_string(3, this->message); buffer.encode_bool(4, this->send_failed); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsResponse {\n"); @@ -2050,10 +2395,6 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->level)); out.append("\n"); - out.append(" tag: "); - out.append("'").append(this->tag).append("'"); - out.append("\n"); - out.append(" message: "); out.append("'").append(this->message).append("'"); out.append("\n"); @@ -2063,10 +2404,13 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); } +#endif bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2085,6 +2429,7 @@ void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceMap::dump_to(std::string &out) const { char buffer[64]; out.append("HomeassistantServiceMap {\n"); @@ -2097,6 +2442,7 @@ void HomeassistantServiceMap::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -2142,6 +2488,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(5, this->is_event); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HomeassistantServiceResponse {\n"); @@ -2172,10 +2519,13 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } +#endif bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2194,6 +2544,7 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeHomeAssistantStateResponse {\n"); @@ -2206,6 +2557,7 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2229,6 +2581,7 @@ void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_string(3, this->attribute); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeAssistantStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HomeAssistantStateResponse {\n"); @@ -2245,8 +2598,11 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } +#endif bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -2258,6 +2614,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } +#ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeResponse::dump_to(std::string &out) const { char buffer[64]; out.append("GetTimeResponse {\n"); @@ -2267,6 +2624,7 @@ void GetTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2291,6 +2649,7 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_enum(2, this->type); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesArgument::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesServicesArgument {\n"); @@ -2303,6 +2662,7 @@ void ListEntitiesServicesArgument::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2334,6 +2694,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(3, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesServicesResponse {\n"); @@ -2353,6 +2714,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { } out.append("}"); } +#endif bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2426,6 +2788,7 @@ void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceArgument::dump_to(std::string &out) const { char buffer[64]; out.append("ExecuteServiceArgument {\n"); @@ -2479,6 +2842,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { } out.append("}"); } +#endif bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -2505,6 +2869,7 @@ void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ExecuteServiceRequest {\n"); @@ -2520,6 +2885,17 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { } out.append("}"); } +#endif +bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2553,7 +2929,9 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesCameraResponse {\n"); @@ -2573,8 +2951,13 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append(" unique_id: "); out.append("'").append(this->unique_id).append("'"); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -2610,6 +2993,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->data); buffer.encode_bool(3, this->done); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageResponse::dump_to(std::string &out) const { char buffer[64]; out.append("CameraImageResponse {\n"); @@ -2627,6 +3011,7 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2645,6 +3030,7 @@ void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->single); buffer.encode_bool(2, this->stream); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageRequest::dump_to(std::string &out) const { char buffer[64]; out.append("CameraImageRequest {\n"); @@ -2657,6 +3043,7 @@ void CameraImageRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -2672,7 +3059,7 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v return true; } case 11: { - this->supports_away = value.as_bool(); + this->legacy_supports_away = value.as_bool(); return true; } case 12: { @@ -2691,6 +3078,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supported_presets.push_back(value.as_enum()); return true; } + case 18: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -2717,6 +3108,10 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe this->supported_custom_presets.push_back(value.as_string()); return true; } + case 19: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -2756,7 +3151,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_temperature_step); - buffer.encode_bool(11, this->supports_away); + buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { buffer.encode_enum(13, it, true); @@ -2773,7 +3168,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_custom_presets) { buffer.encode_string(17, it, true); } + buffer.encode_bool(18, this->disabled_by_default); + buffer.encode_string(19, this->icon); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesClimateResponse {\n"); @@ -2823,8 +3221,8 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" supports_away: "); - out.append(YESNO(this->supports_away)); + out.append(" legacy_supports_away: "); + out.append(YESNO(this->legacy_supports_away)); out.append("\n"); out.append(" supports_action: "); @@ -2860,8 +3258,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); out.append("}"); } +#endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2869,7 +3276,7 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 7: { - this->away = value.as_bool(); + this->legacy_away = value.as_bool(); return true; } case 8: { @@ -2939,7 +3346,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->target_temperature); buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); - buffer.encode_bool(7, this->away); + buffer.encode_bool(7, this->legacy_away); buffer.encode_enum(8, this->action); buffer.encode_enum(9, this->fan_mode); buffer.encode_enum(10, this->swing_mode); @@ -2947,6 +3354,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ClimateStateResponse {\n"); @@ -2979,8 +3387,8 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" away: "); - out.append(YESNO(this->away)); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); out.append("\n"); out.append(" action: "); @@ -3008,6 +3416,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -3031,11 +3440,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) return true; } case 10: { - this->has_away = value.as_bool(); + this->has_legacy_away = value.as_bool(); return true; } case 11: { - this->away = value.as_bool(); + this->legacy_away = value.as_bool(); return true; } case 12: { @@ -3120,8 +3529,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_bool(8, this->has_target_temperature_high); buffer.encode_float(9, this->target_temperature_high); - buffer.encode_bool(10, this->has_away); - buffer.encode_bool(11, this->away); + buffer.encode_bool(10, this->has_legacy_away); + buffer.encode_bool(11, this->legacy_away); buffer.encode_bool(12, this->has_fan_mode); buffer.encode_enum(13, this->fan_mode); buffer.encode_bool(14, this->has_swing_mode); @@ -3133,6 +3542,7 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ClimateCommandRequest {\n"); @@ -3176,12 +3586,12 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" has_away: "); - out.append(YESNO(this->has_away)); + out.append(" has_legacy_away: "); + out.append(YESNO(this->has_legacy_away)); out.append("\n"); - out.append(" away: "); - out.append(YESNO(this->away)); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); out.append("\n"); out.append(" has_fan_mode: "); @@ -3225,6 +3635,388 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesNumberResponse::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 ListEntitiesNumberResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + case 6: { + this->min_value = value.as_float(); + return true; + } + case 7: { + this->max_value = value.as_float(); + return true; + } + case 8: { + this->step = value.as_float(); + return true; + } + default: + return false; + } +} +void ListEntitiesNumberResponse::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_float(6, this->min_value); + buffer.encode_float(7, this->max_value); + buffer.encode_float(8, this->step); + buffer.encode_bool(9, this->disabled_by_default); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesNumberResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesNumberResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", 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(" min_value: "); + sprintf(buffer, "%g", this->min_value); + out.append(buffer); + out.append("\n"); + + out.append(" max_value: "); + sprintf(buffer, "%g", this->max_value); + out.append(buffer); + out.append("\n"); + + out.append(" step: "); + sprintf(buffer, "%g", this->step); + out.append(buffer); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + out.append("}"); +} +#endif +bool NumberStateResponse::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 NumberStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->state = value.as_float(); + return true; + } + default: + return false; + } +} +void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->state); + buffer.encode_bool(3, this->missing_state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void NumberStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("NumberStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + sprintf(buffer, "%g", this->state); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + out.append("}"); +} +#endif +bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->state = value.as_float(); + return true; + } + default: + return false; + } +} +void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void NumberCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("NumberCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + sprintf(buffer, "%g", this->state); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSelectResponse::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 6: { + this->options.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesSelectResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSelectResponse::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); + for (auto &it : this->options) { + buffer.encode_string(6, it, true); + } + buffer.encode_bool(7, this->disabled_by_default); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesSelectResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesSelectResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", 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"); + + for (const auto &it : this->options) { + out.append(" options: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + out.append("}"); +} +#endif +bool SelectStateResponse::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 SelectStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectStateResponse::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 SelectStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", 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 SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SelectCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + 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 04d5834572..13a21c4772 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -32,18 +32,37 @@ enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, }; +enum ColorMode : uint32_t { + COLOR_MODE_UNKNOWN = 0, + COLOR_MODE_ON_OFF = 1, + COLOR_MODE_BRIGHTNESS = 2, + COLOR_MODE_WHITE = 7, + COLOR_MODE_COLOR_TEMPERATURE = 11, + COLOR_MODE_COLD_WARM_WHITE = 19, + COLOR_MODE_RGB = 35, + COLOR_MODE_RGB_WHITE = 39, + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47, + COLOR_MODE_RGB_COLD_WARM_WHITE = 51, +}; enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, + STATE_CLASS_TOTAL_INCREASING = 2, +}; +enum SensorLastResetType : uint32_t { + LAST_RESET_NONE = 0, + LAST_RESET_NEVER = 1, + LAST_RESET_AUTO = 2, }; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, LOG_LEVEL_WARN = 2, LOG_LEVEL_INFO = 3, - LOG_LEVEL_DEBUG = 4, - LOG_LEVEL_VERBOSE = 5, - LOG_LEVEL_VERY_VERBOSE = 6, + LOG_LEVEL_CONFIG = 4, + LOG_LEVEL_DEBUG = 5, + LOG_LEVEL_VERBOSE = 6, + LOG_LEVEL_VERY_VERBOSE = 7, }; enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, @@ -90,13 +109,14 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_FAN = 6, }; enum ClimatePreset : uint32_t { - CLIMATE_PRESET_ECO = 0, - CLIMATE_PRESET_AWAY = 1, - CLIMATE_PRESET_BOOST = 2, - CLIMATE_PRESET_COMFORT = 3, - CLIMATE_PRESET_HOME = 4, - CLIMATE_PRESET_SLEEP = 5, - CLIMATE_PRESET_ACTIVITY = 6, + CLIMATE_PRESET_NONE = 0, + CLIMATE_PRESET_HOME = 1, + CLIMATE_PRESET_AWAY = 2, + CLIMATE_PRESET_BOOST = 3, + CLIMATE_PRESET_COMFORT = 4, + CLIMATE_PRESET_ECO = 5, + CLIMATE_PRESET_SLEEP = 6, + CLIMATE_PRESET_ACTIVITY = 7, }; } // namespace enums @@ -105,7 +125,9 @@ class HelloRequest : public ProtoMessage { public: std::string client_info{}; 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; @@ -116,7 +138,9 @@ class HelloResponse : public ProtoMessage { uint32_t api_version_minor{0}; std::string server_info{}; 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; @@ -126,7 +150,9 @@ class ConnectRequest : public ProtoMessage { public: std::string password{}; 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; @@ -135,7 +161,9 @@ class ConnectResponse : public ProtoMessage { public: bool invalid_password{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; @@ -143,35 +171,45 @@ class ConnectResponse : public ProtoMessage { class DisconnectRequest : 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 DisconnectResponse : 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 PingRequest : 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 PingResponse : 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 DeviceInfoRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -187,7 +225,9 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; 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; @@ -196,21 +236,27 @@ class DeviceInfoResponse : public ProtoMessage { class ListEntitiesRequest : 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 ListEntitiesDoneResponse : 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 SubscribeStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -222,8 +268,12 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { std::string unique_id{}; std::string device_class{}; bool is_status_binary_sensor{false}; + bool disabled_by_default{false}; + std::string icon{}; 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; @@ -236,7 +286,9 @@ class BinarySensorStateResponse : public ProtoMessage { bool state{false}; 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; @@ -252,8 +304,12 @@ class ListEntitiesCoverResponse : public ProtoMessage { bool supports_position{false}; bool supports_tilt{false}; std::string device_class{}; + bool disabled_by_default{false}; + std::string icon{}; 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; @@ -268,7 +324,9 @@ class CoverStateResponse : public ProtoMessage { float tilt{0.0f}; enums::CoverOperation 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; @@ -285,7 +343,9 @@ class CoverCommandRequest : public ProtoMessage { float tilt{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; @@ -301,8 +361,12 @@ class ListEntitiesFanResponse : public ProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; + bool disabled_by_default{false}; + std::string icon{}; 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; @@ -318,7 +382,9 @@ class FanStateResponse : public ProtoMessage { enums::FanDirection direction{}; int32_t speed_level{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; @@ -338,7 +404,9 @@ class FanCommandRequest : public ProtoMessage { bool has_speed_level{false}; int32_t speed_level{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; @@ -350,15 +418,20 @@ class ListEntitiesLightResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; - bool supports_brightness{false}; - bool supports_rgb{false}; - bool supports_white_value{false}; - bool supports_color_temperature{false}; + std::vector supported_color_modes{}; + bool legacy_supports_brightness{false}; + bool legacy_supports_rgb{false}; + bool legacy_supports_white_value{false}; + bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; + bool disabled_by_default{false}; + std::string icon{}; 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; @@ -370,14 +443,20 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + enums::ColorMode color_mode{}; + float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; float white{0.0f}; float color_temperature{0.0f}; + float cold_white{0.0f}; + float warm_white{0.0f}; std::string effect{}; 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; @@ -391,6 +470,10 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_mode{false}; + enums::ColorMode color_mode{}; + bool has_color_brightness{false}; + float color_brightness{0.0f}; bool has_rgb{false}; float red{0.0f}; float green{0.0f}; @@ -399,6 +482,10 @@ class LightCommandRequest : public ProtoMessage { float white{0.0f}; bool has_color_temperature{false}; float color_temperature{0.0f}; + bool has_cold_white{false}; + float cold_white{0.0f}; + bool has_warm_white{false}; + float warm_white{0.0f}; bool has_transition_length{false}; uint32_t transition_length{0}; bool has_flash_length{false}; @@ -406,7 +493,9 @@ class LightCommandRequest : public ProtoMessage { bool has_effect{false}; std::string effect{}; 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; @@ -425,8 +514,12 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; + enums::SensorLastResetType legacy_last_reset_type{}; + bool disabled_by_default{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; @@ -439,7 +532,9 @@ class SensorStateResponse : public ProtoMessage { float state{0.0f}; 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; @@ -453,8 +548,11 @@ class ListEntitiesSwitchResponse : public ProtoMessage { std::string unique_id{}; std::string icon{}; bool assumed_state{false}; + bool disabled_by_default{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; @@ -466,7 +564,9 @@ class SwitchStateResponse : public ProtoMessage { uint32_t key{0}; bool 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; @@ -477,7 +577,9 @@ class SwitchCommandRequest : public ProtoMessage { uint32_t key{0}; bool 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; @@ -490,12 +592,16 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { std::string name{}; std::string unique_id{}; std::string icon{}; + bool disabled_by_default{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 TextSensorStateResponse : public ProtoMessage { public: @@ -503,7 +609,9 @@ class TextSensorStateResponse : public ProtoMessage { 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; @@ -515,7 +623,9 @@ class SubscribeLogsRequest : public ProtoMessage { enums::LogLevel level{}; bool dump_config{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; @@ -523,11 +633,12 @@ class SubscribeLogsRequest : public ProtoMessage { class SubscribeLogsResponse : public ProtoMessage { public: enums::LogLevel level{}; - std::string tag{}; std::string message{}; bool send_failed{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; @@ -536,7 +647,9 @@ class SubscribeLogsResponse : public ProtoMessage { class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -545,7 +658,9 @@ class HomeassistantServiceMap : public ProtoMessage { std::string key{}; std::string value{}; 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; @@ -558,7 +673,9 @@ class HomeassistantServiceResponse : public ProtoMessage { std::vector variables{}; bool is_event{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; @@ -567,7 +684,9 @@ class HomeassistantServiceResponse : public ProtoMessage { class SubscribeHomeAssistantStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -576,7 +695,9 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { std::string entity_id{}; std::string attribute{}; 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; @@ -587,7 +708,9 @@ class HomeAssistantStateResponse : public ProtoMessage { std::string state{}; std::string attribute{}; 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; @@ -595,7 +718,9 @@ class HomeAssistantStateResponse : public ProtoMessage { class GetTimeRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -603,7 +728,9 @@ class GetTimeResponse : public ProtoMessage { public: 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; @@ -613,7 +740,9 @@ class ListEntitiesServicesArgument : public ProtoMessage { std::string name{}; enums::ServiceArgType type{}; 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; @@ -625,7 +754,9 @@ class ListEntitiesServicesResponse : public ProtoMessage { uint32_t key{0}; std::vector args{}; 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; @@ -643,7 +774,9 @@ class ExecuteServiceArgument : public ProtoMessage { std::vector float_array{}; std::vector string_array{}; 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; @@ -655,7 +788,9 @@ class ExecuteServiceRequest : public ProtoMessage { uint32_t key{0}; std::vector args{}; 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; @@ -667,12 +802,16 @@ class ListEntitiesCameraResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; + bool disabled_by_default{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 CameraImageResponse : public ProtoMessage { public: @@ -680,7 +819,9 @@ class CameraImageResponse : public ProtoMessage { std::string data{}; bool done{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; @@ -692,7 +833,9 @@ class CameraImageRequest : public ProtoMessage { bool single{false}; bool stream{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; @@ -709,15 +852,19 @@ class ListEntitiesClimateResponse : public ProtoMessage { float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_temperature_step{0.0f}; - bool supports_away{false}; + bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; std::vector supported_swing_modes{}; std::vector supported_custom_fan_modes{}; std::vector supported_presets{}; std::vector supported_custom_presets{}; + bool disabled_by_default{false}; + std::string icon{}; 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; @@ -732,7 +879,7 @@ class ClimateStateResponse : public ProtoMessage { float target_temperature{0.0f}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - bool away{false}; + bool legacy_away{false}; enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; @@ -740,7 +887,9 @@ class ClimateStateResponse : public ProtoMessage { enums::ClimatePreset preset{}; std::string custom_preset{}; 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; @@ -758,8 +907,8 @@ class ClimateCommandRequest : public ProtoMessage { float target_temperature_low{0.0f}; bool has_target_temperature_high{false}; float target_temperature_high{0.0f}; - bool has_away{false}; - bool away{false}; + bool has_legacy_away{false}; + bool legacy_away{false}; bool has_fan_mode{false}; enums::ClimateFanMode fan_mode{}; bool has_swing_mode{false}; @@ -771,13 +920,109 @@ class ClimateCommandRequest : public ProtoMessage { bool has_custom_preset{false}; std::string custom_preset{}; 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 ListEntitiesNumberResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + float min_value{0.0f}; + float max_value{0.0f}; + float step{0.0f}; + bool disabled_by_default{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 NumberStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + float state{0.0f}; + 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_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class NumberCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + float state{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; +}; +class ListEntitiesSelectResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + std::vector options{}; + bool disabled_by_default{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 SelectStateResponse : 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 SelectCommandRequest : 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; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4fade19787..ad2413ea57 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -9,58 +9,82 @@ namespace api { static const char *const TAG = "api.service"; bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 2); } bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 4); } bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 5); } bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 6); } bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 7); } bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 8); } bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 10); } bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 19); } #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 12); } #endif #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 21); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 13); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 22); } #endif @@ -68,13 +92,17 @@ bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse #endif #ifdef USE_FAN bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 14); } #endif #ifdef USE_FAN bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 23); } #endif @@ -82,13 +110,17 @@ bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &ms #endif #ifdef USE_LIGHT bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 15); } #endif #ifdef USE_LIGHT bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 24); } #endif @@ -96,25 +128,33 @@ bool APIServerConnectionBase::send_light_state_response(const LightStateResponse #endif #ifdef USE_SENSOR bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 16); } #endif #ifdef USE_SENSOR bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 25); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 17); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 26); } #endif @@ -122,13 +162,17 @@ bool APIServerConnectionBase::send_switch_state_response(const SwitchStateRespon #endif #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 18); } #endif #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 27); } #endif @@ -136,35 +180,49 @@ bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsRe return this->send_message_(msg, 29); } bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 35); } bool APIServerConnectionBase::send_subscribe_home_assistant_state_response( const SubscribeHomeAssistantStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 39); } bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 36); } bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 37); } bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 41); } #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 43); } #endif #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 44); } #endif @@ -172,87 +230,147 @@ bool APIServerConnectionBase::send_camera_image_response(const CameraImageRespon #endif #ifdef USE_CLIMATE bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 46); } #endif #ifdef USE_CLIMATE bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 47); } #endif #ifdef USE_CLIMATE #endif +#ifdef USE_NUMBER +bool APIServerConnectionBase::send_list_entities_number_response(const ListEntitiesNumberResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_number_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 49); +} +#endif +#ifdef USE_NUMBER +bool APIServerConnectionBase::send_number_state_response(const NumberStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_number_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 50); +} +#endif +#ifdef USE_NUMBER +#endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_list_entities_select_response(const ListEntitiesSelectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_select_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 52); +} +#endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_select_state_response(const SelectStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_select_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 53); +} +#endif +#ifdef USE_SELECT +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str()); +#endif this->on_hello_request(msg); break; } case 3: { ConnectRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str()); +#endif this->on_connect_request(msg); break; } case 5: { DisconnectRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str()); +#endif this->on_disconnect_request(msg); break; } case 6: { DisconnectResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str()); +#endif this->on_disconnect_response(msg); break; } case 7: { PingRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str()); +#endif this->on_ping_request(msg); break; } case 8: { PingResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str()); +#endif this->on_ping_response(msg); break; } case 9: { DeviceInfoRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str()); +#endif this->on_device_info_request(msg); break; } case 11: { ListEntitiesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str()); +#endif this->on_list_entities_request(msg); break; } case 20: { SubscribeStatesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_states_request(msg); break; } case 28: { SubscribeLogsRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_logs_request(msg); break; } @@ -260,7 +378,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_COVER CoverCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); +#endif this->on_cover_command_request(msg); #endif break; @@ -269,7 +389,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_FAN FanCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); +#endif this->on_fan_command_request(msg); #endif break; @@ -278,7 +400,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_LIGHT LightCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); +#endif this->on_light_command_request(msg); #endif break; @@ -287,7 +411,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_SWITCH SwitchCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); +#endif this->on_switch_command_request(msg); #endif break; @@ -295,42 +421,54 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_homeassistant_services_request(msg); break; } case 36: { GetTimeRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); +#endif this->on_get_time_request(msg); break; } case 37: { GetTimeResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str()); +#endif this->on_get_time_response(msg); break; } case 38: { SubscribeHomeAssistantStatesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_home_assistant_states_request(msg); break; } case 40: { HomeAssistantStateResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str()); +#endif this->on_home_assistant_state_response(msg); break; } case 42: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str()); +#endif this->on_execute_service_request(msg); break; } @@ -338,7 +476,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_ESP32_CAMERA CameraImageRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); +#endif this->on_camera_image_request(msg); #endif break; @@ -347,8 +487,32 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_CLIMATE ClimateCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); +#endif this->on_climate_command_request(msg); +#endif + break; + } + case 51: { +#ifdef USE_NUMBER + NumberCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); +#endif + this->on_number_command_request(msg); +#endif + break; + } + case 54: { +#ifdef USE_SELECT + SelectCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); +#endif + this->on_select_command_request(msg); #endif break; } @@ -547,6 +711,32 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest this->climate_command(msg); } #endif +#ifdef USE_NUMBER +void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->number_command(msg); +} +#endif +#ifdef USE_SELECT +void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->select_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 afbe39e314..1b8d990b05 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -111,6 +111,24 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_CLIMATE virtual void on_climate_command_request(const ClimateCommandRequest &value){}; +#endif +#ifdef USE_NUMBER + bool send_list_entities_number_response(const ListEntitiesNumberResponse &msg); +#endif +#ifdef USE_NUMBER + bool send_number_state_response(const NumberStateResponse &msg); +#endif +#ifdef USE_NUMBER + virtual void on_number_command_request(const NumberCommandRequest &value){}; +#endif +#ifdef USE_SELECT + bool send_list_entities_select_response(const ListEntitiesSelectResponse &msg); +#endif +#ifdef USE_SELECT + bool send_select_state_response(const SelectStateResponse &msg); +#endif +#ifdef USE_SELECT + virtual void on_select_command_request(const SelectCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -147,6 +165,12 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_CLIMATE virtual void climate_command(const ClimateCommandRequest &msg) = 0; +#endif +#ifdef USE_NUMBER + virtual void number_command(const NumberCommandRequest &msg) = 0; +#endif +#ifdef USE_SELECT + virtual void select_command(const SelectCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -179,6 +203,12 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_CLIMATE void on_climate_command_request(const ClimateCommandRequest &msg) override; #endif +#ifdef USE_NUMBER + void on_number_command_request(const NumberCommandRequest &msg) override; +#endif +#ifdef USE_SELECT + void on_select_command_request(const SelectCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1373533ae2..4e2899d94f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -1,10 +1,13 @@ #include "api_server.h" #include "api_connection.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/core/util.h" #include "esphome/core/defines.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" @@ -21,24 +24,49 @@ static const char *const TAG = "api"; void APIServer::setup() { ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); this->setup_controller(); - this->server_ = AsyncServer(this->port_); - this->server_.setNoDelay(false); - this->server_.begin(); - this->server_.onClient( - [](void *s, AsyncClient *client) { - if (client == nullptr) - return; + socket_ = socket::socket(AF_INET, SOCK_STREAM, 0); + if (socket_ == nullptr) { + ESP_LOGW(TAG, "Could not create socket."); + this->mark_failed(); + return; + } + int enable = 1; + int err = 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); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_in server; + memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); + + err = socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + return; + } + + err = socket_->listen(4); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->mark_failed(); + return; + } - // can't print here because in lwIP thread - // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str()); - auto *a_this = (APIServer *) s; - a_this->clients_.push_back(new APIConnection(client, a_this)); - }, - this); #ifdef USE_LOGGER if (logger::global_logger != nullptr) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { - for (auto *c : this->clients_) { + for (auto &c : this->clients_) { if (!c->remove_) c->send_log_message(level, tag, message); } @@ -50,30 +78,41 @@ void APIServer::setup() { #ifdef USE_ESP32_CAMERA if (esp32_camera::global_esp32_camera != nullptr) { - esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { - for (auto *c : this->clients_) - if (!c->remove_) - c->send_camera_state(image); - }); + esp32_camera::global_esp32_camera->add_image_callback( + [this](const std::shared_ptr &image) { + for (auto &c : this->clients_) + if (!c->remove_) + c->send_camera_state(image); + }); } #endif } void APIServer::loop() { + // Accept new clients + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + + auto *conn = new APIConnection(std::move(sock), this); + clients_.emplace_back(conn); + conn->start(); + } + // Partition clients into remove and active - auto new_end = - std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); + auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), + [](const std::unique_ptr &conn) { return !conn->remove_; }); // print disconnection messages for (auto it = new_end; it != this->clients_.end(); ++it) { - ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str()); + ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); } - // only then delete the pointers, otherwise log routine - // would access freed memory - for (auto it = new_end; it != this->clients_.end(); ++it) - delete *it; // resize vector this->clients_.erase(new_end, this->clients_.end()); - for (auto *client : this->clients_) { + for (auto &client : this->clients_) { client->loop(); } @@ -93,7 +132,12 @@ void APIServer::loop() { } void APIServer::dump_config() { ESP_LOGCONFIG(TAG, "API Server:"); - ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_); + ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_); +#ifdef USE_API_NOISE + ESP_LOGCONFIG(TAG, " Using noise encryption: YES"); +#else + ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); +#endif } bool APIServer::uses_password() const { return !this->password_.empty(); } bool APIServer::check_password(const std::string &password) const { @@ -129,7 +173,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_binary_sensor_state(obj, state); } #endif @@ -138,7 +182,7 @@ void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s void APIServer::on_cover_update(cover::Cover *obj) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_cover_state(obj); } #endif @@ -147,7 +191,7 @@ void APIServer::on_cover_update(cover::Cover *obj) { void APIServer::on_fan_update(fan::FanState *obj) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_fan_state(obj); } #endif @@ -156,7 +200,7 @@ void APIServer::on_fan_update(fan::FanState *obj) { void APIServer::on_light_update(light::LightState *obj) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_light_state(obj); } #endif @@ -165,7 +209,7 @@ void APIServer::on_light_update(light::LightState *obj) { void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_sensor_state(obj, state); } #endif @@ -174,7 +218,7 @@ void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { void APIServer::on_switch_update(switch_::Switch *obj, bool state) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_switch_state(obj, state); } #endif @@ -183,7 +227,7 @@ void APIServer::on_switch_update(switch_::Switch *obj, bool state) { void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_text_sensor_state(obj, state); } #endif @@ -192,18 +236,36 @@ void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s void APIServer::on_climate_update(climate::Climate *obj) { if (obj->is_internal()) return; - for (auto *c : this->clients_) + for (auto &c : this->clients_) c->send_climate_state(obj); } #endif +#ifdef USE_NUMBER +void APIServer::on_number_update(number::Number *obj, float state) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_number_state(obj, state); +} +#endif + +#ifdef USE_SELECT +void APIServer::on_select_update(select::Select *obj, const std::string &state) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_select_state(obj, state); +} +#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) void APIServer::set_password(const std::string &password) { this->password_ = password; } void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - for (auto *client : this->clients_) { + for (auto &client : this->clients_) { client->send_homeassistant_service_call(call); } } @@ -223,7 +285,7 @@ uint16_t APIServer::get_port() const { return this->port_; } void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { - for (auto *client : this->clients_) { + for (auto &client : this->clients_) { if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED) client->send_time_request(); } @@ -231,7 +293,7 @@ void APIServer::request_time() { #endif bool APIServer::is_connected() const { return !this->clients_.empty(); } void APIServer::on_shutdown() { - for (auto *c : this->clients_) { + for (auto &c : this->clients_) { c->send_disconnect_request(DisconnectRequest()); } delay(10); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index eb6c91d01c..056d9f54f2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,20 +4,14 @@ #include "esphome/core/controller.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" +#include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" #include "util.h" #include "list_entities.h" #include "subscribe_state.h" -#include "homeassistant_service.h" #include "user_services.h" - -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif +#include "api_noise_context.h" namespace esphome { namespace api { @@ -36,6 +30,12 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); + +#ifdef USE_API_NOISE + void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } + std::shared_ptr get_noise_ctx() { return noise_ctx_; } +#endif // USE_API_NOISE + void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; @@ -60,6 +60,12 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; +#endif +#ifdef USE_NUMBER + void on_number_update(number::Number *obj, float state) override; +#endif +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } @@ -81,14 +87,18 @@ class APIServer : public Component, public Controller { const std::vector &get_user_services() const { return this->user_services_; } protected: - AsyncServer server_{0}; + std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; uint32_t reboot_timeout_{300000}; uint32_t last_connected_{0}; - std::vector clients_; + std::vector> clients_; std::string password_; std::vector state_subs_; std::vector user_services_; + +#ifdef USE_API_NOISE + std::shared_ptr noise_ctx_ = std::make_shared(); +#endif // USE_API_NOISE }; extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py new file mode 100644 index 0000000000..4a3944d33e --- /dev/null +++ b/esphome/components/api/client.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from datetime import datetime +from typing import Optional + +from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel +import zeroconf + +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): + conf = config["api"] + port: int = int(conf[CONF_PORT]) + password: str = conf[CONF_PASSWORD] + noise_psk: Optional[str] = None + if CONF_ENCRYPTION in conf: + noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + _LOGGER.info("Starting log output from %s using esphome API", address) + zc = zeroconf.Zeroconf() + cli = APIClient( + asyncio.get_event_loop(), + address, + port, + password, + client_info=f"ESPHome Logs {__version__}", + noise_psk=noise_psk, + ) + first_connect = True + + 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() + + try: + while True: + await asyncio.sleep(60) + except KeyboardInterrupt: + await reconnect.stop() + zc.close() + + +def run_logs(config, address): + asyncio.run(async_run_logs(config, address)) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 74343904eb..9f125a6149 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -49,7 +49,7 @@ class CustomAPIDevice { template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { - auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); + auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } @@ -72,7 +72,7 @@ class CustomAPIDevice { * @param name The name of the arguments for the service, must match the arguments of the function. */ template void register_service(void (T::*callback)(), const std::string &name) { - auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); + auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index d4245136ae..745dd92c89 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -51,5 +51,13 @@ bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); } #endif +#ifdef USE_NUMBER +bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } +#endif + +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 6b10a72fdf..c728fb0a97 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -39,6 +39,12 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_CLIMATE bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif bool on_end() override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 0d2eb2d279..0ba277d90a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -80,11 +80,13 @@ void ProtoMessage::decode(const uint8_t *buffer, size_t length) { } } +#ifdef HAS_PROTO_MESSAGE_DUMP std::string ProtoMessage::dump() const { std::string out; this->dump_to(out); return out; } +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index f1486a2511..edbc916b01 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,8 +1,13 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define HAS_PROTO_MESSAGE_DUMP +#endif + namespace esphome { namespace api { @@ -241,10 +246,13 @@ class ProtoWriteBuffer { class ProtoMessage { public: + virtual ~ProtoMessage() = default; virtual void encode(ProtoWriteBuffer buffer) const = 0; void decode(const uint8_t *buffer, size_t length); +#ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; +#endif protected: virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 2612a852d3..1b8453f233 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -37,6 +37,16 @@ bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) #ifdef USE_CLIMATE bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } #endif +#ifdef USE_NUMBER +bool InitialStateIterator::on_number(number::Number *number) { + return this->client_->send_number_state(number, number->state); +} +#endif +#ifdef USE_SELECT +bool InitialStateIterator::on_select(select::Select *select) { + return this->client_->send_select_state(select, select->state); +} +#endif InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 51b9c695e4..beb9b947d4 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -36,6 +36,12 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_CLIMATE bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 39e42bcc02..49618f5467 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -15,7 +15,7 @@ template<> std::string get_execute_arg_value(const ExecuteServiceAr template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.bool_array; } -template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { +template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.int_array; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index f929db5d6a..5085994607 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -167,6 +167,36 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_NUMBER + case IteratorState::NUMBER: + if (this->at_ >= App.get_numbers().size()) { + advance_platform = true; + } else { + auto *number = App.get_numbers()[this->at_]; + if (number->is_internal()) { + success = true; + break; + } else { + success = this->on_number(number); + } + } + break; +#endif +#ifdef USE_SELECT + case IteratorState::SELECT: + if (this->at_ >= App.get_selects().size()) { + advance_platform = true; + } else { + auto *select = App.get_selects()[this->at_]; + if (select->is_internal()) { + success = true; + break; + } else { + success = this->on_select(select); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index 5a29a48cbe..e404a95619 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -47,6 +47,12 @@ class ComponentIterator { #endif #ifdef USE_CLIMATE virtual bool on_climate(climate::Climate *climate) = 0; +#endif +#ifdef USE_NUMBER + virtual bool on_number(number::Number *number) = 0; +#endif +#ifdef USE_SELECT + virtual bool on_select(select::Select *select) = 0; #endif virtual bool on_end(); @@ -81,6 +87,12 @@ class ComponentIterator { #endif #ifdef USE_CLIMATE CLIMATE, +#endif +#ifdef USE_NUMBER + NUMBER, +#endif +#ifdef USE_SELECT + SELECT, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index d0e53e7832..2e65aab4d1 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h" diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index a571121742..271a29e0fc 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor from esphome.const import ( CONF_DISTANCE, CONF_LIGHTNING_ENERGY, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_KILOMETER, - UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH, ) @@ -19,14 +17,15 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( - UNIT_KILOMETER, - ICON_SIGNAL_DISTANCE_VARIANT, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER, + icon=ICON_SIGNAL_DISTANCE_VARIANT, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( - UNIT_EMPTY, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp index 1a1cd8fe82..3a7fa7bf84 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.cpp +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -25,8 +25,12 @@ void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t I2CAS3935Component::read_register(uint8_t reg) { uint8_t value; - if (!this->read_byte(reg, &value, 2)) { - ESP_LOGW(TAG, "Read failed!"); + if (write(®, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Writing register failed!"); + return 0; + } + if (read(&value, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading register failed!"); return 0; } return value; diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 8938dc4671..8789448792 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,9 +1,15 @@ # Dummy integration to allow relying on AsyncTCP import esphome.codegen as cg +import esphome.config_validation as cv from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@OttoWinter"] +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_arduino, +) + @coroutine_with_priority(200.0) async def to_code(config): @@ -12,4 +18,4 @@ async def to_code(config): cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP - cg.add_library("ESPAsyncTCP-esphome", "1.2.3") + cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3") diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index 5656cdf430..42c30598ad 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -1,7 +1,7 @@ #include "atc_mithermometer.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace atc_mithermometer { @@ -25,14 +25,14 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device bool success = false; for (auto &service_data : device.get_service_datas()) { - auto res = parse_header(service_data); - if (res->is_duplicate) { + auto res = parse_header_(service_data); + if (!res.has_value()) { continue; } - if (!(parse_message(service_data.data, *res))) { + if (!(parse_message_(service_data.data, *res))) { continue; } - if (!(report_results(res, device.address_str()))) { + if (!(report_results_(res, device.address_str()))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -46,14 +46,10 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device success = true; } - if (!success) { - return false; - } - - return true; + return success; } -optional ATCMiThermometer::parse_header(const esp32_ble_tracker::ServiceData &service_data) { +optional ATCMiThermometer::parse_header_(const esp32_ble_tracker::ServiceData &service_data) { ParseResult result; if (!service_data.uuid.contains(0x1A, 0x18)) { ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); @@ -64,17 +60,15 @@ optional ATCMiThermometer::parse_header(const esp32_ble_tracker::Se static uint8_t last_frame_count = 0; if (last_frame_count == raw[12]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); - result.is_duplicate = true; + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); return {}; } last_frame_count = raw[12]; - result.is_duplicate = false; return result; } -bool ATCMiThermometer::parse_message(const std::vector &message, ParseResult &result) { +bool ATCMiThermometer::parse_message_(const std::vector &message, ParseResult &result) { // Byte 0-5 mac in correct order // Byte 6-7 Temperature in uint16 // Byte 8 Humidity in percent @@ -107,7 +101,7 @@ bool ATCMiThermometer::parse_message(const std::vector &message, ParseR return true; } -bool ATCMiThermometer::report_results(const optional &result, const std::string &address) { +bool ATCMiThermometer::report_results_(const optional &result, const std::string &address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_results(): no results available."); return false; diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 203dca3200..ca079bf8c1 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace atc_mithermometer { @@ -14,7 +14,6 @@ struct ParseResult { optional humidity; optional battery_level; optional battery_voltage; - bool is_duplicate; int raw_offset; }; @@ -37,9 +36,9 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *battery_level_{nullptr}; sensor::Sensor *battery_voltage_{nullptr}; - optional parse_header(const esp32_ble_tracker::ServiceData &service_data); - bool parse_message(const std::vector &message, ParseResult &result); - bool report_results(const optional &result, const std::string &address); + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message_(const std::vector &message, ParseResult &result); + bool report_results_(const optional &result, const std::string &address); }; } // namespace atc_mithermometer diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index efa3f2b51a..0f6cc1abcb 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,28 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(ATCMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index ebceaa817c..e4b8448da6 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -265,27 +265,57 @@ float ATM90E32Component::get_power_factor_c_() { } float ATM90E32Component::get_forward_active_energy_a_() { uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA); - return (float) val * 10 / 3200; // convert register value to WattHours + 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); - return (float) val * 10 / 3200; + 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); - return (float) val * 10 / 3200; + 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); - return (float) val * 10 / 3200; + 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); - return (float) val * 10 / 3200; + 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); - return (float) val * 10 / 3200; + 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_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 89d62adaf6..c9662df26e 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -77,6 +77,8 @@ class ATM90E32Component : public PollingComponent, 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}; } phase_[3]; sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index dc2048fbc2..7927a7fdfb 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -39,7 +39,7 @@ static const uint16_t ATM90E32_STATUS_S0_OVPHASEBST = 1 << 11; // Over voltage static const uint16_t ATM90E32_STATUS_S0_OVPHASECST = 1 << 10; // Over voltage on phase C static const uint16_t ATM90E32_STATUS_S0_UREVWNST = 1 << 9; // Voltage Phase Sequence Error status static const uint16_t ATM90E32_STATUS_S0_IREVWNST = 1 << 8; // Current Phase Sequence Error status -static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater tha INWarnTh reg +static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater than INWarnTh reg static const uint16_t ATM90E32_STATUS_S0_TQNOLOADST = 1 << 6; // All phase sum reactive power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TPNOLOADST = 1 << 5; // All phase sum active power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TASNOLOADST = 1 << 4; // All phase sum apparent power no-load status diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 68ec199bff..05e5250d89 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -12,21 +12,19 @@ from esphome.const import ( CONF_FORWARD_ACTIVE_ENERGY, CONF_REVERSE_ACTIVE_ENERGY, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, ICON_LIGHTBULB, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT_HOURS, @@ -64,37 +62,45 @@ ATM90E32Component = atm90e32_ns.class_( ATM90E32_PHASE_SCHEMA = cv.Schema( { cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_EMPTY, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + 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_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + 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_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + 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_VOLT_AMPS_REACTIVE, - ICON_LIGHTBULB, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + 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_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, @@ -109,18 +115,16 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 81c9243bdb..ee12226977 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -1,7 +1,7 @@ #include "b_parasite.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace b_parasite { @@ -14,6 +14,7 @@ void BParasite::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Soil Moisture", this->soil_moisture_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); } bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { @@ -36,6 +37,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; + const uint8_t protocol_version = data[0] >> 4; + if (protocol_version != 1) { + ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); + return false; + } + + // Some b-parasite versions have an (optional) illuminance sensor. + bool has_illuminance = data[0] & 0x1; + // Counter for deduplicating messages. uint8_t counter = data[1] & 0x0f; if (last_processed_counter_ == counter) { @@ -47,7 +57,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { uint16_t battery_millivolt = data[2] << 8 | data[3]; float battery_voltage = battery_millivolt / 1000.0f; - // Temperature in 1000 * Celcius. + // Temperature in 1000 * Celsius. uint16_t temp_millicelcius = data[4] << 8 | data[5]; float temp_celcius = temp_millicelcius / 1000.0f; @@ -59,6 +69,9 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { uint16_t soil_moisture = data[8] << 8 | data[9]; float moisture_percent = (100.0f * soil_moisture) / (1 << 16); + // Ambient light in lux. + float illuminance = has_illuminance ? data[16] << 8 | data[17] : 0.0f; + if (battery_voltage_ != nullptr) { battery_voltage_->publish_state(battery_voltage); } @@ -71,6 +84,13 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (soil_moisture_ != nullptr) { soil_moisture_->publish_state(moisture_percent); } + if (illuminance_ != nullptr) { + if (has_illuminance) { + illuminance_->publish_state(illuminance); + } else { + ESP_LOGE(TAG, "No lux information is present in the BLE packet"); + } + } last_processed_counter_ = counter; return true; @@ -79,4 +99,4 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { } // namespace b_parasite } // namespace esphome -#endif // ARDUINO_ARCH_ESP32 +#endif // USE_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 04f648ab63..70ee4ab23c 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace b_parasite { @@ -22,6 +22,7 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_soil_moisture(sensor::Sensor *soil_moisture) { soil_moisture_ = soil_moisture; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } protected: // The received advertisement packet contains an unsigned 4 bits wrap-around counter @@ -32,9 +33,10 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; sensor::Sensor *soil_moisture_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; }; } // namespace b_parasite } // namespace esphome -#endif // ARDUINO_ARCH_ESP32 +#endif // USE_ESP32 diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index d93e41816b..d51c48c602 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -5,15 +5,17 @@ from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_HUMIDITY, CONF_ID, + CONF_ILLUMINANCE, CONF_MOISTURE, CONF_MAC_ADDRESS, CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + UNIT_LUX, UNIT_PERCENT, UNIT_VOLT, ) @@ -33,28 +35,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(BParasite), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + 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, ), } ) @@ -75,6 +83,7 @@ async def to_code(config): (CONF_HUMIDITY, var.set_humidity), (CONF_BATTERY_VOLTAGE, var.set_battery_voltage), (CONF_MOISTURE, var.set_soil_moisture), + (CONF_ILLUMINANCE, var.set_illuminance), ]: if config_key in config: sens = await sensor.new_sensor(config[config_key]) diff --git a/esphome/components/xiaomi_mijia/__init__.py b/esphome/components/ballu/__init__.py similarity index 100% rename from esphome/components/xiaomi_mijia/__init__.py rename to esphome/components/ballu/__init__.py diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp new file mode 100644 index 0000000000..e2703a79fb --- /dev/null +++ b/esphome/components/ballu/ballu.cpp @@ -0,0 +1,239 @@ +#include "ballu.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ballu { + +static const char *const TAG = "ballu.climate"; + +const uint16_t BALLU_HEADER_MARK = 9000; +const uint16_t BALLU_HEADER_SPACE = 4500; +const uint16_t BALLU_BIT_MARK = 575; +const uint16_t BALLU_ONE_SPACE = 1675; +const uint16_t BALLU_ZERO_SPACE = 550; + +const uint32_t BALLU_CARRIER_FREQUENCY = 38000; + +const uint8_t BALLU_STATE_LENGTH = 13; + +const uint8_t BALLU_AUTO = 0; +const uint8_t BALLU_COOL = 0x20; +const uint8_t BALLU_DRY = 0x40; +const uint8_t BALLU_HEAT = 0x80; +const uint8_t BALLU_FAN = 0xc0; + +const uint8_t BALLU_FAN_AUTO = 0xa0; +const uint8_t BALLU_FAN_HIGH = 0x20; +const uint8_t BALLU_FAN_MED = 0x40; +const uint8_t BALLU_FAN_LOW = 0x60; + +const uint8_t BALLU_SWING_VER = 0x07; +const uint8_t BALLU_SWING_HOR = 0xe0; +const uint8_t BALLU_POWER = 0x20; + +void BalluClimate::transmit_state() { + uint8_t remote_state[BALLU_STATE_LENGTH] = {0}; + + auto temp = (uint8_t) roundf(clamp(this->target_temperature, YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX)); + auto swing_ver = + ((this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)); + auto swing_hor = + ((this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)); + + remote_state[0] = 0xc3; + remote_state[1] = ((temp - 8) << 3) | (swing_ver ? 0 : BALLU_SWING_VER); + remote_state[2] = swing_hor ? 0 : BALLU_SWING_HOR; + remote_state[9] = (this->mode == climate::CLIMATE_MODE_OFF) ? 0 : BALLU_POWER; + remote_state[11] = 0x1e; + + // Fan speed + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_HIGH: + remote_state[4] |= BALLU_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[4] |= BALLU_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[4] |= BALLU_FAN_LOW; + break; + case climate::CLIMATE_FAN_AUTO: + remote_state[4] |= BALLU_FAN_AUTO; + break; + default: + break; + } + + // Mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + remote_state[6] |= BALLU_AUTO; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[6] |= BALLU_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[6] |= BALLU_COOL; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[6] |= BALLU_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[6] |= BALLU_FAN; + break; + case climate::CLIMATE_MODE_OFF: + remote_state[6] |= BALLU_AUTO; + default: + break; + } + + // Checksum + for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++) + remote_state[12] += remote_state[i]; + + ESP_LOGV(TAG, "Sending: %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]); + + // Send code + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(BALLU_HEADER_MARK); + data->space(BALLU_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(BALLU_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? BALLU_ONE_SPACE : BALLU_ZERO_SPACE); + } + } + // Footer + data->mark(BALLU_BIT_MARK); + + transmit.perform(); +} + +bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(BALLU_HEADER_MARK, BALLU_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[BALLU_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < BALLU_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) + remote_state[i] |= 1 << j; + + else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + + ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]); + } + // Validate footer + if (!data.expect_mark(BALLU_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // Calculate checksum and compare with signal value. + for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++) + checksum += remote_state[i]; + + if (checksum != remote_state[BALLU_STATE_LENGTH - 1]) { + ESP_LOGVV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %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]); + + // verify header remote code + if (remote_state[0] != 0xc3) + return false; + + // powr on/off button + ESP_LOGV(TAG, "Power: %02X", (remote_state[9] & BALLU_POWER)); + + if ((remote_state[9] & BALLU_POWER) != BALLU_POWER) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0xe0; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case BALLU_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case BALLU_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case BALLU_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case BALLU_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case BALLU_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + + // Set received temp + int temp = remote_state[1] & 0xf8; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + temp = ((uint8_t) temp >> 3) + 8; + ESP_LOGVV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = remote_state[4] & 0xe0; + ESP_LOGVV(TAG, "Fan: %02X", fan); + switch (fan) { + case BALLU_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case BALLU_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case BALLU_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case BALLU_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + ESP_LOGVV(TAG, "Swing status: %02X %02X", remote_state[1] & BALLU_SWING_VER, remote_state[2] & BALLU_SWING_HOR); + if (((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) && + ((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR)) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else if ((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if ((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + + this->publish_state(); + return true; +} + +} // namespace ballu +} // namespace esphome diff --git a/esphome/components/ballu/ballu.h b/esphome/components/ballu/ballu.h new file mode 100644 index 0000000000..80a4699cfb --- /dev/null +++ b/esphome/components/ballu/ballu.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace ballu { + +// Support for Ballu air conditioners with YKR-K/002E remote + +// Temperature +const float YKR_K_002E_TEMP_MIN = 16.0; +const float YKR_K_002E_TEMP_MAX = 32.0; + +class BalluClimate : public climate_ir::ClimateIR { + public: + BalluClimate() + : climate_ir::ClimateIR(YKR_K_002E_TEMP_MIN, YKR_K_002E_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}) {} + + 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 ballu +} // namespace esphome diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py new file mode 100644 index 0000000000..82e9fead1e --- /dev/null +++ b/esphome/components/ballu/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 + +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@bazuchan"] + +ballu_ns = cg.esphome_ns.namespace("ballu") +BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BalluClimate), + } +) + + +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/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index ca361c7012..5645f46f1c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -21,7 +21,12 @@ void BangBangClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_AUTO; + if (supports_cool_ && supports_heat_) + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + else if (supports_cool_) + this->mode = climate::CLIMATE_MODE_COOL; + else if (supports_heat_) + this->mode = climate::CLIMATE_MODE_HEAT; this->change_away_(false); } } @@ -32,8 +37,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) - this->change_away_(*call.get_away()); + if (call.get_preset().has_value()) + this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); this->compute_state_(); this->publish_state(); @@ -41,24 +46,31 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + }); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_cool_ && supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); traits.set_supports_two_point_target_temperature(true); - traits.set_supports_away(this->supports_away_); + if (supports_away_) + traits.set_supported_presets({ + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + }); traits.set_supports_action(true); return traits; } void BangBangClimate::compute_state_() { - if (this->mode != climate::CLIMATE_MODE_AUTO) { - // in non-auto mode, switch directly to appropriate action - // - HEAT mode -> HEATING action - // - COOL mode -> COOLING action - // - OFF mode -> OFF action (not IDLE!) - this->switch_to_action_(static_cast(this->mode)); + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { + if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature_low) || + std::isnan(this->target_temperature_high)) { // if any control parameters are nan, go to OFF action (not IDLE!) this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; @@ -140,7 +152,7 @@ void BangBangClimate::change_away_(bool away) { this->target_temperature_low = this->away_config_.default_temperature_low; this->target_temperature_high = this->away_config_.default_temperature_high; } - this->away = away; + this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 43af116c9e..951fe3670c 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -71,10 +71,11 @@ void BH1750Sensor::update() { float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } void BH1750Sensor::read_data_() { uint16_t raw_value; - if (!this->parent_->raw_receive_16(this->address_, &raw_value, 1)) { + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { this->status_set_warning(); return; } + raw_value = i2c::i2ctohs(raw_value); float lx = float(raw_value) / 1.2f; lx *= 69.0f / this->measurement_duration_; diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index e688241dcc..156c7bb375 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_RESOLUTION, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, CONF_MEASUREMENT_DURATION, @@ -28,7 +27,10 @@ BH1750Sensor = bh1750_ns.class_( CONF_MEASUREMENT_TIME = "measurement_time" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index eaf41829bb..2201fe576e 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -55,7 +55,10 @@ void BinaryFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace binary } // namespace esphome diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 731973bdad..86c83aff5c 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -12,7 +12,7 @@ class BinaryLightOutput : public light::LightOutput { void set_output(output::BinaryOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(false); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 68d4d3e324..ec199cc5fa 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +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 @@ -8,7 +9,6 @@ from esphome.const import ( CONF_DEVICE_CLASS, CONF_FILTERS, CONF_ID, - CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERTED, CONF_MAX_LENGTH, @@ -22,7 +22,6 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_FOR, CONF_NAME, CONF_MQTT_ID, DEVICE_CLASS_EMPTY, @@ -48,6 +47,7 @@ from esphome.const import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -79,6 +79,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] @@ -86,7 +87,7 @@ DEVICE_CLASSES = [ IS_PLATFORM_COMPONENT = True binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") -BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.Nameable) +BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) BinarySensorInitiallyOff = binary_sensor_ns.class_( "BinarySensorInitiallyOff", BinarySensor ) @@ -229,17 +230,16 @@ def parse_multi_click_timing_str(value): parts = value.lower().split(" ") if len(parts) != 5: raise cv.Invalid( - "Multi click timing grammar consists of exactly 5 words, not {}" - "".format(len(parts)) + f"Multi click timing grammar consists of exactly 5 words, not {len(parts)}" ) try: state = cv.boolean(parts[0]) except cv.Invalid: # pylint: disable=raise-missing-from - raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) + raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}") if parts[1] != "for": - raise cv.Invalid("Second word must be 'for', got {}".format(parts[1])) + raise cv.Invalid(f"Second word must be 'for', got {parts[1]}") if parts[2] == "at": if parts[3] == "least": @@ -248,8 +248,7 @@ def parse_multi_click_timing_str(value): key = CONF_MAX_LENGTH else: raise cv.Invalid( - "Third word after at must either be 'least' or 'most', got {}" - "".format(parts[3]) + f"Third word after at must either be 'least' or 'most', got {parts[3]}" ) try: length = cv.positive_time_period_milliseconds(parts[4]) @@ -294,13 +293,11 @@ def validate_multi_click_timing(value): new_state = v_.get(CONF_STATE, not state) if new_state == state: raise cv.Invalid( - "Timings must have alternating state. Indices {} and {} have " - "the same state {}".format(i, i + 1, state) + f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}" ) if max_length is not None and max_length < min_length: raise cv.Invalid( - "Max length ({}) must be larger than min length ({})." - "".format(max_length, min_length) + f"Max length ({max_length}) must be larger than min length ({min_length})." ) state = new_state @@ -316,7 +313,7 @@ def validate_multi_click_timing(value): device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +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( @@ -372,19 +369,13 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), } ), - cv.Optional(CONF_INVERTED): cv.invalid( - "The inverted binary_sensor property has been replaced by the " - "new 'invert' binary sensor filter. Please see " - "https://esphome.io/components/binary_sensor/index.html." - ), } ) async def setup_binary_sensor_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) + await setup_entity(var, config) + if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) if CONF_INVERTED in config: @@ -455,10 +446,6 @@ async def new_binary_sensor(config): BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(BinarySensor), - cv.Optional(CONF_FOR): cv.invalid( - "This option has been removed in 1.13, please use the " - "'for' condition instead." - ), } ) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index a333b33397..ce082aafb3 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -80,6 +80,10 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_not_valid"); } void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { + if (min_length == 0) { + this->is_valid_ = true; + return; + } this->is_valid_ = false; this->set_timeout("is_valid", min_length, [this]() { ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 0c1e80afba..31bf1a5565 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -4,6 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/hal.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 2e1f228be6..41da83aa3e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -42,7 +42,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { } } std::string BinarySensor::device_class() { return ""; } -BinarySensor::BinarySensor(const std::string &name) : Nameable(name), state(false) {} +BinarySensor::BinarySensor(const std::string &name) : EntityBase(name), state(false) {} BinarySensor::BinarySensor() : BinarySensor("") {} void BinarySensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } std::string BinarySensor::get_device_class() { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 62e8031cb5..9c0d43fa98 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/filter.h" @@ -10,7 +11,7 @@ namespace binary_sensor { #define LOG_BINARY_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + 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()); \ } \ @@ -22,7 +23,7 @@ namespace binary_sensor { * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public Nameable { +class BinarySensor : public EntityBase { public: explicit BinarySensor(); /** Construct a binary sensor with the specified name diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 131a050052..946e2f9e62 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -7,8 +7,6 @@ from esphome.const import ( CONF_CHANNELS, CONF_VALUE, CONF_TYPE, - DEVICE_CLASS_EMPTY, - UNIT_EMPTY, ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP, @@ -35,11 +33,9 @@ entry = { CONFIG_SCHEMA = cv.typed_schema( { CONF_GROUP: sensor.sensor_schema( - UNIT_EMPTY, - ICON_CHECK_CIRCLE_OUTLINE, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_CHECK_CIRCLE_OUTLINE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(BinarySensorMap), diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 2db609de55..6c374046ba 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -3,7 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_client { @@ -11,11 +11,12 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { public: explicit BLEClientConnectTrigger(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) { + 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_OPEN_EVT && param->open.status == ESP_GATT_OK) this->trigger(); if (event == ESP_GATTC_SEARCH_CMPL_EVT) - this->node_state = espbt::ClientState::Established; + this->node_state = espbt::ClientState::ESTABLISHED; } }; @@ -23,11 +24,12 @@ 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) { + 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_->remote_bda, 6) == 0) this->trigger(); if (event == ESP_GATTC_SEARCH_CMPL_EVT) - this->node_state = espbt::ClientState::Established; + this->node_state = espbt::ClientState::ESTABLISHED; } }; diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 956848b73d..8ff516d735 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -4,7 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "ble_client.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_client { @@ -17,12 +17,12 @@ void BLEClient::setup() { ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); this->mark_failed(); } - this->set_states(espbt::ClientState::Idle); + this->set_states_(espbt::ClientState::IDLE); this->enabled = true; } void BLEClient::loop() { - if (this->state() == espbt::ClientState::Discovered) { + if (this->state() == espbt::ClientState::DISCOVERED) { this->connect(); } for (auto *node : this->nodes_) @@ -39,11 +39,11 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { return false; if (device.address_uint64() != this->address) return false; - if (this->state() != espbt::ClientState::Idle) + if (this->state() != espbt::ClientState::IDLE) return false; ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); - this->set_states(espbt::ClientState::Discovered); + this->set_states_(espbt::ClientState::DISCOVERED); auto addr = device.address_uint64(); this->remote_bda[0] = (addr >> 40) & 0xFF; @@ -69,7 +69,7 @@ std::string BLEClient::address_str() const { void BLEClient::set_enabled(bool enabled) { if (enabled == this->enabled) return; - if (!enabled && this->state() != espbt::ClientState::Idle) { + 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) { @@ -84,9 +84,9 @@ void BLEClient::connect() { auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true); if (ret) { ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); - this->set_states(espbt::ClientState::Idle); + this->set_states_(espbt::ClientState::IDLE); } else { - this->set_states(espbt::ClientState::Connecting); + this->set_states_(espbt::ClientState::CONNECTING); } } @@ -97,7 +97,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if) return; - bool all_established = this->all_nodes_established(); + bool all_established = this->all_nodes_established_(); switch (event) { case ESP_GATTC_REG_EVT: { @@ -113,7 +113,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); if (param->open.status != ESP_GATT_OK) { ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); - this->set_states(espbt::ClientState::Idle); + this->set_states_(espbt::ClientState::IDLE); break; } this->conn_id = param->open.conn_id; @@ -126,11 +126,11 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es case ESP_GATTC_CFG_MTU_EVT: { if (param->cfg_mtu.status != ESP_GATT_OK) { ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); - this->set_states(espbt::ClientState::Idle); + this->set_states_(espbt::ClientState::IDLE); break; } ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); - esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, NULL); + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } case ESP_GATTC_DISCONNECT_EVT: { @@ -139,13 +139,13 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); for (auto &svc : this->services_) - delete svc; + delete svc; // NOLINT(cppcoreguidelines-owning-memory) this->services_.clear(); - this->set_states(espbt::ClientState::Idle); + this->set_states_(espbt::ClientState::IDLE); break; } case ESP_GATTC_SEARCH_RES_EVT: { - BLEService *ble_service = new BLEService(); + BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); ble_service->start_handle = param->search_res.start_handle; ble_service->end_handle = param->search_res.end_handle; @@ -160,8 +160,8 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); svc->parse_characteristics(); } - this->set_states(espbt::ClientState::Connected); - this->set_state(espbt::ClientState::Established); + this->set_states_(espbt::ClientState::CONNECTED); + this->set_state(espbt::ClientState::ESTABLISHED); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -192,9 +192,9 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es 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()) { + if (!all_established && this->all_nodes_established_()) { for (auto &svc : this->services_) - delete svc; + delete svc; // NOLINT(cppcoreguidelines-owning-memory) this->services_.clear(); } } @@ -307,7 +307,7 @@ BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_ BLEService::~BLEService() { for (auto &chr : this->characteristics) - delete chr; + delete chr; // NOLINT(cppcoreguidelines-owning-memory) } void BLEService::parse_characteristics() { @@ -329,7 +329,7 @@ void BLEService::parse_characteristics() { break; } - BLECharacteristic *characteristic = new BLECharacteristic(); + BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory) characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); characteristic->properties = result.properties; characteristic->handle = result.char_handle; @@ -344,7 +344,7 @@ void BLEService::parse_characteristics() { BLECharacteristic::~BLECharacteristic() { for (auto &desc : this->descriptors) - delete desc; + delete desc; // NOLINT(cppcoreguidelines-owning-memory) } void BLECharacteristic::parse_descriptors() { @@ -366,7 +366,7 @@ void BLECharacteristic::parse_descriptors() { break; } - BLEDescriptor *desc = new BLEDescriptor(); + BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); desc->handle = result.handle; desc->characteristic = this; diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 203acc181f..4a17ccb79b 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include @@ -25,7 +25,7 @@ class BLEClientNode { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; - virtual void loop() = 0; + virtual void loop(){}; void set_address(uint64_t address) { address_ = address; } espbt::ESPBTClient *client; // This should be transitioned to Established once the node no longer needs @@ -82,10 +82,11 @@ class BLEClient : public espbt::ESPBTClient, public Component { void dump_config() override; 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); + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; bool parse_device(const espbt::ESPBTDevice &device) override; void on_scan_end() override {} - void connect(); + void connect() override; void set_address(uint64_t address) { this->address = address; } @@ -116,16 +117,16 @@ class BLEClient : public espbt::ESPBTClient, public Component { std::string address_str() const; protected: - void set_states(espbt::ClientState st) { + void set_states_(espbt::ClientState st) { this->set_state(st); for (auto &node : nodes_) node->node_state = st; } - bool all_nodes_established() { - if (this->state() != espbt::ClientState::Established) + bool all_nodes_established_() { + if (this->state() != espbt::ClientState::ESTABLISHED) return false; for (auto &node : nodes_) - if (node->node_state != espbt::ClientState::Established) + if (node->node_state != espbt::ClientState::ESTABLISHED) return false; return true; } diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index c6f05932ef..efe4bf0e9a 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -2,12 +2,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( - DEVICE_CLASS_EMPTY, CONF_ID, CONF_LAMBDA, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, CONF_TRIGGER_ID, CONF_SERVICE_UUID, ) @@ -34,7 +31,8 @@ BLESensorNotifyTrigger = ble_client_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index a528493947..2baaafe2ec 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -3,7 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/sensor/ble_sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_client { @@ -11,10 +11,11 @@ namespace ble_client { class BLESensorNotifyTrigger : public Trigger, public BLESensor { public: explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + 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; + this->sensor_->node_state = espbt::ClientState::ESTABLISHED; break; } case ESP_GATTC_NOTIFY_EVT: { diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 270822be9d..7a2e3ddc8b 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_client { @@ -71,7 +71,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); } } else { - this->node_state = espbt::ClientState::Established; + this->node_state = espbt::ClientState::ESTABLISHED; } break; } @@ -84,7 +84,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } if (param->read.handle == this->handle) { this->status_clear_warning(); - this->publish_state(this->parse_data(param->read.value, param->read.value_len)); + this->publish_state(this->parse_data_(param->read.value, param->read.value_len)); } break; } @@ -93,11 +93,11 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); - this->publish_state(this->parse_data(param->notify.value, param->notify.value_len)); + this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len)); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->node_state = espbt::ClientState::Established; + this->node_state = espbt::ClientState::ESTABLISHED; break; } default: @@ -105,7 +105,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } } -float BLESensor::parse_data(uint8_t *value, uint16_t value_len) { +float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { if (this->data_to_value_func_.has_value()) { std::vector data(value, value + value_len); return (*this->data_to_value_func_)(data); @@ -115,7 +115,7 @@ float BLESensor::parse_data(uint8_t *value, uint16_t value_len) { } void BLESensor::update() { - if (this->node_state != espbt::ClientState::Established) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); return; } diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index 25e996b6ee..d9f310b575 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/sensor/sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include namespace esphome { @@ -32,13 +32,13 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void set_data_to_value(data_to_value_t &&lambda_) { this->data_to_value_func_ = lambda_; } + void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } void set_enable_notify(bool notify) { this->notify_ = notify; } uint16_t handle; protected: uint32_t hash_base() override; - float parse_data(uint8_t *value, uint16_t value_len); + float parse_data_(uint8_t *value, uint16_t value_len); optional data_to_value_func_{}; bool notify_; espbt::ESPBTUUID service_uuid_; diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 669984d705..6de5252404 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_client { @@ -21,10 +21,10 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i this->publish_state(this->parent_->enabled); break; case ESP_GATTC_OPEN_EVT: - this->node_state = espbt::ClientState::Established; + this->node_state = espbt::ClientState::ESTABLISHED; break; case ESP_GATTC_DISCONNECT_EVT: - this->node_state = espbt::ClientState::Idle; + this->node_state = espbt::ClientState::IDLE; this->publish_state(this->parent_->enabled); break; default: diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index f91af533f1..2e19c8aeef 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/switch/switch.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include namespace esphome { diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index c58d29e6be..2a242c3aca 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, + CONF_IBEACON_MAJOR, + CONF_IBEACON_MINOR, + CONF_IBEACON_UUID, + CONF_ID, +) DEPENDENCIES = ["esp32_ble_tracker"] @@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_( esp32_ble_tracker.ESPBTDeviceListener, ) + +def _validate(config): + if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon major identifier requires iBeacon UUID") + if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID") + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.BINARY_SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(BLEPresenceDevice), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, 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, } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), + cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + _validate, ) @@ -50,5 +70,15 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_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])) + 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 CONF_IBEACON_MINOR in config: + cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) diff --git a/esphome/components/ble_presence/ble_presence_device.cpp b/esphome/components/ble_presence/ble_presence_device.cpp index 1355c2bcc3..e482bb9a78 100644 --- a/esphome/components/ble_presence/ble_presence_device.cpp +++ b/esphome/components/ble_presence/ble_presence_device.cpp @@ -1,7 +1,7 @@ #include "ble_presence_device.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_presence { diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index bce6a9cf98..dcccf844d2 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -4,7 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_presence { @@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public Component { public: void set_address(uint64_t address) { - this->by_address_ = true; + this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } void set_service_uuid16(uint16_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); } + void set_ibeacon_uuid(uint8_t *uuid) { + this->match_by_ = MATCH_BY_IBEACON_UUID; + this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } + void set_ibeacon_major(uint16_t major) { + this->check_ibeacon_major_ = true; + this->ibeacon_major_ = major; + } + void set_ibeacon_minor(uint16_t minor) { + this->check_ibeacon_minor_ = true; + this->ibeacon_minor_ = minor; + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (this->by_address_) { - if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; - return true; - } - } else { - for (auto uuid : device.get_service_uuids()) { - if (this->uuid_ == uuid) { - this->publish_state(device.get_rssi()); + switch (this->match_by_) { + case MATCH_BY_MAC_ADDRESS: + if (device.address_uint64() == this->address_) { + this->publish_state(true); this->found_ = true; return true; } - } + break; + case MATCH_BY_SERVICE_UUID: + for (auto uuid : device.get_service_uuids()) { + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } + break; + case MATCH_BY_IBEACON_UUID: + if (!device.get_ibeacon().has_value()) { + return false; + } + + auto ibeacon = device.get_ibeacon().value(); + + if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { + return false; + } + + if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) { + return false; + } + + if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) { + return false; + } + + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; } return false; } @@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, 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 }; + MatchType match_by_; + bool found_{false}; - bool by_address_{false}; + uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; + + esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; + uint16_t ibeacon_major_; + bool check_ibeacon_major_; + uint16_t ibeacon_minor_; + bool check_ibeacon_minor_; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.cpp b/esphome/components/ble_rssi/ble_rssi_sensor.cpp index 096259d8d1..4b37fcc6ef 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.cpp +++ b/esphome/components/ble_rssi/ble_rssi_sensor.cpp @@ -1,7 +1,7 @@ #include "ble_rssi_sensor.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_rssi { diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 2082a52469..c6acae2593 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/sensor/sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_rssi { diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 819b7c6fd7..0c4308b11a 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, - ICON_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -20,11 +19,10 @@ BLERSSISensor = ble_rssi_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_DECIBEL, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -62,5 +60,5 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/ble_scanner/ble_scanner.cpp b/esphome/components/ble_scanner/ble_scanner.cpp index 798824bb4e..f2cda227bb 100644 --- a/esphome/components/ble_scanner/ble_scanner.cpp +++ b/esphome/components/ble_scanner/ble_scanner.cpp @@ -1,7 +1,7 @@ #include "ble_scanner.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_scanner { diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 194494144c..b330eff696 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -7,7 +7,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/text_sensor/text_sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ble_scanner { @@ -15,7 +15,7 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - this->publish_state("{\"timestamp\":" + to_string(::time(NULL)) + + this->publish_state("{\"timestamp\":" + to_string(::time(nullptr)) + "," "\"address\":\"" + device.address_str() + diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index 097974da7a..18386430a2 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -113,14 +113,14 @@ void BME280Component::setup() { 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_register = 0; - if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_register)) { + uint8_t humid_control_val = 0; + if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { this->mark_failed(); return; } - humid_register &= ~0b00000111; - humid_register |= this->humidity_oversampling_ & 0b111; - if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_register)) { + 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; } @@ -169,11 +169,11 @@ inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return ( void BME280Component::update() { // Enable sensor ESP_LOGV(TAG, "Sending conversion request..."); - uint8_t meas_register = 0; - meas_register |= (this->temperature_oversampling_ & 0b111) << 5; - meas_register |= (this->pressure_oversampling_ & 0b111) << 2; - meas_register |= BME280_MODE_FORCED; - if (!this->write_byte(BME280_REGISTER_CONTROL, meas_register)) { + 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; } @@ -186,7 +186,7 @@ void BME280Component::update() { this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { int32_t t_fine = 0; float temperature = this->read_temperature_(&t_fine); - if (isnan(temperature)) { + if (std::isnan(temperature)) { ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); this->status_set_warning(); return; diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py index 8c6cc7ae56..dcb842d879 100644 --- a/esphome/components/bme280/sensor.py +++ b/esphome/components/bme280/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_HECTOPASCAL, @@ -49,11 +48,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( @@ -62,11 +60,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + 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( @@ -75,11 +72,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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( diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index ee7db3c65f..99e0b6f860 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -1,5 +1,6 @@ #include "bme680.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace bme680 { diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index eaa158c9f8..76472c7562 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -20,7 +19,6 @@ from esphome.const import ( UNIT_OHM, ICON_GAS_CYLINDER, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, UNIT_PERCENT, ) @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME680Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( @@ -72,11 +69,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + 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( @@ -85,11 +81,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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( @@ -98,11 +93,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( - UNIT_OHM, - ICON_GAS_CYLINDER, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( IIR_FILTER_OPTIONS, upper=True diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 8f53180296..0a8ca7f3c3 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; -BME680BSECComponent *BME680BSECComponent::instance; +BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void BME680BSECComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); @@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va sensor->publish_state(value); } -void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } @@ -381,7 +381,7 @@ void BME680BSECComponent::delay_ms(uint32_t period) { void BME680BSECComponent::load_state_() { uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); - this->bsec_state_ = global_preferences.make_preference(hash, true); + this->bsec_state_ = global_preferences->make_preference(hash, true); uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; if (this->bsec_state_.load(&state)) { diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 73994b7541..53bc5c3280 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -5,6 +5,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/preferences.h" +#include "esphome/core/defines.h" #include #ifdef USE_BSEC @@ -70,7 +71,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { int64_t get_time_ns_(); void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); - void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); void load_state_(); void save_state_(uint8_t accuracy); diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 4520bf3480..8d00012150 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -6,13 +6,11 @@ from esphome.const import ( CONF_HUMIDITY, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - UNIT_EMPTY, UNIT_HECTOPASCAL, UNIT_OHM, UNIT_PARTS_PER_MILLION, @@ -54,54 +52,60 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_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_PERCENT, - ICON_WATER_PERCENT, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + 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_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py index 1b48f2e440..52f554120a 100644 --- a/esphome/components/bmp085/sensor.py +++ b/esphome/components/bmp085/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP085Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280/bmp280.cpp index 4048e40077..b4348e8a74 100644 --- a/esphome/components/bmp280/bmp280.cpp +++ b/esphome/components/bmp280/bmp280.cpp @@ -123,11 +123,11 @@ inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return ( void BMP280Component::update() { // Enable sensor ESP_LOGV(TAG, "Sending conversion request..."); - uint8_t meas_register = 0; - meas_register |= (this->temperature_oversampling_ & 0b111) << 5; - meas_register |= (this->pressure_oversampling_ & 0b111) << 2; - meas_register |= 0b01; // Forced mode - if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_register)) { + 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; } @@ -139,7 +139,7 @@ void BMP280Component::update() { this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { int32_t t_fine = 0; float temperature = this->read_temperature_(&t_fine); - if (isnan(temperature)) { + if (std::isnan(temperature)) { ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); this->status_set_warning(); return; diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py index 48953d0259..95a9577f7e 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, CONF_IIR_FILTER, CONF_OVERSAMPLING, @@ -46,11 +45,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + 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( diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 102bfd370e..384a3f23a0 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -3,7 +3,7 @@ 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.core import coroutine_with_priority +from esphome.core import coroutine_with_priority, CORE AUTO_LOAD = ["web_server_base"] DEPENDENCIES = ["wifi"] @@ -12,14 +12,17 @@ CODEOWNERS = ["@OttoWinter"] captive_portal_ns = cg.esphome_ns.namespace("captive_portal") CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CaptivePortal), - cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( - web_server_base.WebServerBase - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CaptivePortal), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( + web_server_base.WebServerBase + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) @coroutine_with_priority(64.0) @@ -29,3 +32,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) 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) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 746029e011..9e00adae3d 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "captive_portal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -76,16 +78,16 @@ void CaptivePortal::start() { this->base_->add_ota_handler(); } - this->dns_server_ = new DNSServer(); + this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); - IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", ip); + network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); + this->dns_server_->start(53, "*", (uint32_t) ip); this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { bool not_found = false; if (!this->active_) { not_found = true; - } else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) { + } else if (req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { not_found = true; } @@ -94,8 +96,8 @@ void CaptivePortal::start() { return; } - auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString(); - req->redirect(url); + auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); + req->redirect(url.c_str()); }); this->initialized_ = true; @@ -151,3 +153,5 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 44b1050f45..b308de42b7 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,5 +1,8 @@ #pragma once +#ifdef USE_ARDUINO + +#include #include #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -26,7 +29,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { this->active_ = false; this->base_->deinit(); this->dns_server_->stop(); - delete this->dns_server_; + this->dns_server_ = nullptr; } bool canHandle(AsyncWebServerRequest *request) override { @@ -65,10 +68,12 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; - DNSServer *dns_server_{nullptr}; + std::unique_ptr dns_server_{nullptr}; }; extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace captive_portal } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index dec070a9b2..11a66f5100 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -1,5 +1,6 @@ #include "ccs811.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ccs811 { @@ -16,7 +17,7 @@ static const char *const TAG = "ccs811"; return; \ } -#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED) +#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED) void CCS811Component::setup() { // page 9 programming guide - hwid is always 0x81 @@ -38,12 +39,14 @@ void CCS811Component::setup() { // set MEAS_MODE (page 5) uint8_t meas_mode = 0; uint32_t interval = this->get_update_interval(); - if (interval <= 1000) - meas_mode = 1 << 4; - else if (interval <= 10000) - meas_mode = 2 << 4; + if (interval >= 60 * 1000) + meas_mode = 3 << 4; // sensor takes a reading every 60 seconds + else if (interval >= 10 * 1000) + meas_mode = 2 << 4; // sensor takes a reading every 10 seconds + else if (interval >= 1 * 1000) + meas_mode = 1 << 4; // sensor takes a reading every second else - meas_mode = 3 << 4; + meas_mode = 4 << 4; // sensor takes a reading every 250ms CHECKED_IO(this->write_byte(0x01, meas_mode)) @@ -51,10 +54,43 @@ void CCS811Component::setup() { // baseline available, write to sensor this->write_bytes(0x11, decode_uint16(*this->baseline_)); } + + auto hardware_version_data = this->read_bytes<1>(0x21); + auto bootloader_version_data = this->read_bytes<2>(0x23); + auto application_version_data = this->read_bytes<2>(0x24); + + uint8_t hardware_version = 0; + uint16_t bootloader_version = 0; + uint16_t application_version = 0; + + if (hardware_version_data.has_value()) { + hardware_version = (*hardware_version_data)[0]; + } + + if (bootloader_version_data.has_value()) { + bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]); + } + + if (application_version_data.has_value()) { + application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]); + } + + ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version, + bootloader_version, application_version); + if (this->version_ != nullptr) { + char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room + sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), + (application_version >> 4 & 15), application_version); + ESP_LOGD(TAG, "publishing version state: %s", version); + this->version_->publish_state(version); + } } void CCS811Component::update() { - if (!this->status_has_data_()) + if (!this->status_has_data_()) { + ESP_LOGD(TAG, "Status indicates no data ready!"); this->status_set_warning(); + return; + } // page 12 - alg result data auto alg_data = this->read_bytes<4>(0x02); @@ -92,12 +128,12 @@ void CCS811Component::send_env_data_() { float humidity = NAN; if (this->humidity_ != nullptr) humidity = this->humidity_->state; - if (isnan(humidity) || humidity < 0 || humidity > 100) + if (std::isnan(humidity) || humidity < 0 || humidity > 100) humidity = 50; float temperature = NAN; if (this->temperature_ != nullptr) temperature = this->temperature_->state; - if (isnan(temperature) || temperature < -25 || temperature > 50) + if (std::isnan(temperature) || temperature < -25 || temperature > 50) temperature = 25; // temperature has a 25° offset to allow negative temperatures temperature += 25; @@ -117,6 +153,7 @@ void CCS811Component::dump_config() { LOG_UPDATE_INTERVAL(this) LOG_SENSOR(" ", "CO2 Sensor", this->co2_) LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) + LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) if (this->baseline_) { ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); } else { @@ -124,7 +161,7 @@ void CCS811Component::dump_config() { } if (this->is_failed()) { switch (this->error_code_) { - case COMMUNICAITON_FAILED: + case COMMUNICATION_FAILED: ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); break; case INVALID_ID: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index cea919c9a5..8a0d60d002 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { public: void set_co2(sensor::Sensor *co2) { co2_ = co2; } void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_version(text_sensor::TextSensor *version) { version_ = version; } void set_baseline(uint16_t baseline) { baseline_ = baseline; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } @@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { enum ErrorCode { UNKNOWN, - COMMUNICAITON_FAILED, + COMMUNICATION_FAILED, INVALID_ID, SENSOR_REPORTED_ERROR, APP_INVALID, @@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; + text_sensor::TextSensor *version_{nullptr}; optional baseline_{}; /// Input sensor for humidity reading. sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4c4f8802d4..bb8200273d 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -1,19 +1,27 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, text_sensor from esphome.const import ( + CONF_ICON, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, + ICON_RESTART, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, + CONF_BASELINE, + CONF_ECO2, CONF_TEMPERATURE, CONF_TVOC, CONF_HUMIDITY, + CONF_VERSION, ICON_MOLECULE_CO2, ) +AUTO_LOAD = ["text_sensor"] +CODEOWNERS = ["@habbie"] DEPENDENCIES = ["i2c"] ccs811_ns = cg.esphome_ns.namespace("ccs811") @@ -21,26 +29,29 @@ CCS811Component = ccs811_ns.class_( "CCS811Component", cg.PollingComponent, i2c.I2CDevice ) -CONF_ECO2 = "eco2" -CONF_BASELINE = "baseline" - CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(CCS811Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, + } ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), @@ -62,6 +73,11 @@ 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 = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) + await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) + cg.add(var.set_version(sens)) + if CONF_BASELINE in config: cg.add(var.set_baseline(config[CONF_BASELINE])) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 5a4492216e..7ff769e5cb 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,26 +1,41 @@ 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 from esphome.const import ( + CONF_ACTION_STATE_TOPIC, CONF_AWAY, + CONF_AWAY_COMMAND_TOPIC, + CONF_AWAY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, + CONF_FAN_MODE, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_FAN_MODE_STATE_TOPIC, CONF_ID, - CONF_INTERNAL, CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, CONF_MODE, + CONF_MODE_COMMAND_TOPIC, + CONF_MODE_STATE_TOPIC, CONF_PRESET, + CONF_SWING_MODE, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_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_TEMPERATURE_STEP, CONF_VISUAL, CONF_MQTT_ID, - CONF_NAME, - CONF_FAN_MODE, - CONF_SWING_MODE, ) from esphome.core import CORE, coroutine_with_priority @@ -29,14 +44,14 @@ IS_PLATFORM_COMPONENT = True CODEOWNERS = ["@esphome/core"] climate_ns = cg.esphome_ns.namespace("climate") -Climate = climate_ns.class_("Climate", cg.Nameable) +Climate = climate_ns.class_("Climate", cg.EntityBase) ClimateCall = climate_ns.class_("ClimateCall") ClimateTraits = climate_ns.class_("ClimateTraits") ClimateMode = climate_ns.enum("ClimateMode") CLIMATE_MODES = { "OFF": ClimateMode.CLIMATE_MODE_OFF, - "HEAT_COOL": ClimateMode.CLIMATE_HEAT_COOL, + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, "COOL": ClimateMode.CLIMATE_MODE_COOL, "HEAT": ClimateMode.CLIMATE_MODE_HEAT, "DRY": ClimateMode.CLIMATE_MODE_DRY, @@ -62,6 +77,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) ClimatePreset = climate_ns.enum("ClimatePreset") CLIMATE_PRESETS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, @@ -86,7 +102,7 @@ validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) -CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +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), @@ -97,15 +113,61 @@ CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature, } ), - # TODO: MQTT topic options + 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_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 + ), } ) async def setup_climate_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) + 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])) @@ -118,6 +180,82 @@ async def setup_climate_core_(var, config): mqtt_ = cg.new_Pvariable(config[CONF_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: + cg.add( + mqtt_.set_custom_current_temperature_state_topic( + config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] + ) + ) + if CONF_FAN_MODE_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_fan_mode_command_topic( + config[CONF_FAN_MODE_COMMAND_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 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_state_topic(config[CONF_MODE_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: + cg.add( + mqtt_.set_custom_target_temperature_command_topic( + config[CONF_TARGET_TEMPERATURE_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_TEMPERATURE_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_temperature_state_topic( + config[CONF_TARGET_TEMPERATURE_STATE_TOPIC] + ) + ) + if CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_target_temperature_high_command_topic( + config[CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_temperature_high_state_topic( + config[CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC] + ) + ) + if CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_target_temperature_low_command_topic( + config[CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_temperature_state_topic( + config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] + ) + ) + async def register_climate(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index b0b71cb7d7..49a87027f2 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -27,7 +27,9 @@ 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_away(this->away_.optional_value(x...)); + if (away_.has_value()) { + call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); + } call.set_fan_mode(this->fan_mode_.optional_value(x...)); call.set_fan_mode(this->custom_fan_mode_.optional_value(x...)); call.set_preset(this->preset_.optional_value(x...)); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 49ed8d922c..34e6328d8a 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -9,8 +9,8 @@ void ClimateCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { - const char *mode_s = climate_mode_to_string(*this->mode_); - ESP_LOGD(TAG, " Mode: %s", mode_s); + const LogString *mode_s = climate_mode_to_string(*this->mode_); + ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } if (this->custom_fan_mode_.has_value()) { this->fan_mode_.reset(); @@ -18,8 +18,8 @@ void ClimateCall::perform() { } if (this->fan_mode_.has_value()) { this->custom_fan_mode_.reset(); - const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", fan_mode_s); + const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); + ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } if (this->custom_preset_.has_value()) { this->preset_.reset(); @@ -27,12 +27,12 @@ void ClimateCall::perform() { } if (this->preset_.has_value()) { this->custom_preset_.reset(); - const char *preset_s = climate_preset_to_string(*this->preset_); - ESP_LOGD(TAG, " Preset: %s", preset_s); + const LogString *preset_s = climate_preset_to_string(*this->preset_); + ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } if (this->swing_mode_.has_value()) { - const char *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); - ESP_LOGD(TAG, " Swing: %s", swing_mode_s); + const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); + ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); } if (this->target_temperature_.has_value()) { ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); @@ -43,9 +43,6 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } - if (this->away_.has_value()) { - ESP_LOGD(TAG, " Away Mode: %s", ONOFF(*this->away_)); - } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -53,7 +50,7 @@ void ClimateCall::validate_() { if (this->mode_.has_value()) { auto mode = *this->mode_; if (!traits.supports_mode(mode)) { - ESP_LOGW(TAG, " Mode %s is not supported by this device!", climate_mode_to_string(mode)); + ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode))); this->mode_.reset(); } } @@ -66,7 +63,8 @@ void ClimateCall::validate_() { } else if (this->fan_mode_.has_value()) { auto fan_mode = *this->fan_mode_; if (!traits.supports_fan_mode(fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", climate_fan_mode_to_string(fan_mode)); + ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", + LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); this->fan_mode_.reset(); } } @@ -79,14 +77,15 @@ void ClimateCall::validate_() { } else if (this->preset_.has_value()) { auto preset = *this->preset_; if (!traits.supports_preset(preset)) { - ESP_LOGW(TAG, " Preset %s is not supported by this device!", climate_preset_to_string(preset)); + ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset))); this->preset_.reset(); } } if (this->swing_mode_.has_value()) { auto swing_mode = *this->swing_mode_; if (!traits.supports_swing_mode(swing_mode)) { - ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", climate_swing_mode_to_string(swing_mode)); + ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", + LOG_STR_ARG(climate_swing_mode_to_string(swing_mode))); this->swing_mode_.reset(); } } @@ -96,7 +95,7 @@ void ClimateCall::validate_() { ESP_LOGW(TAG, " Cannot set target temperature for climate device " "with two-point target temperature!"); this->target_temperature_.reset(); - } else if (isnan(target)) { + } else if (std::isnan(target)) { ESP_LOGW(TAG, " Target temperature must not be NAN!"); this->target_temperature_.reset(); } @@ -108,11 +107,11 @@ void ClimateCall::validate_() { this->target_temperature_high_.reset(); } } - if (this->target_temperature_low_.has_value() && isnan(*this->target_temperature_low_)) { + if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) { ESP_LOGW(TAG, " Target temperature low must not be NAN!"); this->target_temperature_low_.reset(); } - if (this->target_temperature_high_.has_value() && isnan(*this->target_temperature_high_)) { + if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) { ESP_LOGW(TAG, " Target temperature low must not be NAN!"); this->target_temperature_high_.reset(); } @@ -125,12 +124,6 @@ void ClimateCall::validate_() { this->target_temperature_high_.reset(); } } - if (this->away_.has_value()) { - if (!traits.get_supports_away()) { - ESP_LOGW(TAG, " Cannot set away mode for this device!"); - this->away_.reset(); - } - } } ClimateCall &ClimateCall::set_mode(ClimateMode mode) { this->mode_ = mode; @@ -181,8 +174,7 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { this->set_fan_mode(CLIMATE_FAN_DIFFUSE); } else { - auto custom_fan_modes = this->parent_->get_traits().get_supported_custom_fan_modes(); - if (std::find(custom_fan_modes.begin(), custom_fan_modes.end(), fan_mode) != custom_fan_modes.end()) { + if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { this->custom_fan_mode_ = fan_mode; this->fan_mode_.reset(); } else { @@ -218,8 +210,7 @@ ClimateCall &ClimateCall::set_preset(const std::string &preset) { } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { this->set_preset(CLIMATE_PRESET_ACTIVITY); } else { - auto custom_presets = this->parent_->get_traits().get_supported_custom_presets(); - if (std::find(custom_presets.begin(), custom_presets.end(), preset) != custom_presets.end()) { + if (this->parent_->get_traits().supports_custom_preset(preset)) { this->custom_preset_ = preset; this->preset_.reset(); } else { @@ -269,18 +260,23 @@ 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_away() const { return this->away_; } +optional ClimateCall::get_away() const { + if (!this->preset_.has_value()) + return {}; + return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY; +} 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_; } const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } ClimateCall &ClimateCall::set_away(bool away) { - this->away_ = away; + this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; return *this; } ClimateCall &ClimateCall::set_away(optional away) { - this->away_ = away; + if (away.has_value()) + this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; return *this; } ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { @@ -318,17 +314,27 @@ void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +// Random 32bit value; If this changes existing restore preferences are invalidated +static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; + optional Climate::restore_state_() { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ + RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; return recovered; } void Climate::save_state_() { +#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY) +#pragma GCC diagnostic ignored "-Wclass-memaccess" +#endif ClimateDeviceRestoreState state{}; // initialize as zero to prevent random data on stack triggering erase memset(&state, 0, sizeof(ClimateDeviceRestoreState)); +#if USE_ESP_IDF && !defined(CLANG_TIDY) +#pragma GCC diagnostic pop +#endif state.mode = this->mode; auto traits = this->get_traits(); @@ -338,20 +344,17 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } - if (traits.get_supports_away()) { - state.away = this->away; - } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { state.uses_custom_fan_mode = true; - auto &custom_fan_modes = traits.get_supported_custom_fan_modes(); - auto it = std::find(custom_fan_modes.begin(), custom_fan_modes.end(), this->custom_fan_mode.value()); - // only set custom fan mode if value exists, otherwise leave it as is - if (it != custom_fan_modes.cend()) { - state.custom_fan_mode = std::distance(custom_fan_modes.begin(), it); + const auto &supported = traits.get_supported_custom_fan_modes(); + std::vector vec{supported.begin(), supported.end()}; + auto it = std::find(vec.begin(), vec.end(), custom_fan_mode); + if (it != vec.end()) { + state.custom_fan_mode = std::distance(vec.begin(), it); } } if (traits.get_supports_presets() && preset.has_value()) { @@ -360,11 +363,12 @@ void Climate::save_state_() { } if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { state.uses_custom_preset = true; - auto custom_presets = traits.get_supported_custom_presets(); - auto it = std::find(custom_presets.begin(), custom_presets.end(), this->custom_preset.value()); + const auto &supported = traits.get_supported_custom_presets(); + std::vector vec{supported.begin(), supported.end()}; + auto it = std::find(vec.begin(), vec.end(), custom_preset); // only set custom preset if value exists, otherwise leave it as is - if (it != custom_presets.cend()) { - state.custom_preset = std::distance(custom_presets.begin(), it); + if (it != vec.cend()) { + state.custom_preset = std::distance(vec.begin(), it); } } if (traits.get_supports_swing_modes()) { @@ -377,24 +381,24 @@ void Climate::publish_state() { ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); auto traits = this->get_traits(); - ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode)); + ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); if (traits.get_supports_action()) { - ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); + ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { - ESP_LOGD(TAG, " Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode.value())); + ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); } if (traits.get_supports_presets() && this->preset.has_value()) { - ESP_LOGD(TAG, " Preset: %s", climate_preset_to_string(this->preset.value())); + ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); } if (traits.get_supports_swing_modes()) { - ESP_LOGD(TAG, " Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode)); + ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); @@ -405,9 +409,6 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } - if (traits.get_supports_away()) { - ESP_LOGD(TAG, " Away: %s", ONOFF(this->away)); - } // Send state to frontend this->state_callback_.call(); @@ -439,7 +440,7 @@ void Climate::set_visual_max_temperature_override(float visual_max_temperature_o void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) { this->visual_temperature_step_override_ = visual_temperature_step_override; } -Climate::Climate(const std::string &name) : Nameable(name) {} +Climate::Climate(const std::string &name) : EntityBase(name) {} Climate::Climate() : Climate("") {} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -453,9 +454,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } - if (traits.get_supports_away()) { - call.set_away(this->away); - } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -476,23 +474,27 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } - if (traits.get_supports_away()) { - climate->away = this->away; - } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { - climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + // std::set has consistent order (lexicographic for strings), so this is ok + const auto &modes = traits.get_supported_custom_fan_modes(); + std::vector modes_vec{modes.begin(), modes.end()}; + if (custom_fan_mode < modes_vec.size()) { + climate->custom_fan_mode = modes_vec[this->custom_fan_mode]; + } } if (traits.get_supports_presets() && !this->uses_custom_preset) { climate->preset = this->preset; } - if (!traits.get_supported_custom_presets().empty() && this->uses_custom_preset) { - climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; - } if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { - climate->custom_preset = traits.get_supported_custom_presets()[this->preset]; + // std::set has consistent order (lexicographic for strings), so this is ok + const auto &presets = traits.get_supported_custom_presets(); + std::vector presets_vec{presets.begin(), presets.end()}; + if (custom_preset < presets_vec.size()) { + climate->custom_preset = presets_vec[this->custom_preset]; + } } if (traits.get_supports_swing_modes()) { climate->swing_mode = this->swing_mode; @@ -500,5 +502,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } +template bool set_alternative(optional &dst, optional &alt, const T1 &src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_alternative(this->fan_mode, this->custom_fan_mode, mode); +} + +bool Climate::set_custom_fan_mode_(const std::string &mode) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +} + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } + +bool Climate::set_custom_preset_(const std::string &preset) { + return set_alternative(this->custom_preset, this->preset, preset); +} + +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: %.1f", traits.get_visual_temperature_step()); + if (traits.get_supports_current_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + if (traits.get_supports_two_point_target_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); + if (traits.get_supports_action()) + ESP_LOGCONFIG(tag, " [x] Supports action"); + if (!traits.get_supported_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported modes:"); + for (ClimateMode m : traits.get_supported_modes()) + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m))); + } + if (!traits.get_supported_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported fan modes:"); + for (ClimateFanMode m : traits.get_supported_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); + } + if (!traits.get_supported_custom_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:"); + for (const std::string &s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported presets:"); + for (ClimatePreset p : traits.get_supported_presets()) + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p))); + } + if (!traits.get_supported_custom_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom presets:"); + for (const std::string &s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_swing_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported swing modes:"); + for (ClimateSwingMode m : traits.get_supported_swing_modes()) + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 3ac9270341..852b76686c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/core/log.h" @@ -12,7 +13,7 @@ namespace climate { #define LOG_CLIMATE(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ } class Climate; @@ -63,7 +64,9 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") ClimateCall &set_away(bool away); + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") ClimateCall &set_away(optional away); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); @@ -94,7 +97,8 @@ 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_away() const; + ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20") + optional get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -109,7 +113,6 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; - optional away_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -118,9 +121,9 @@ class ClimateCall { }; /// Struct used to save the state of the climate device in restore memory. +/// Make sure to update RESTORE_STATE_VERSION when changing the struct entries. struct ClimateDeviceRestoreState { ClimateMode mode; - bool away; bool uses_custom_fan_mode{false}; union { ClimateFanMode fan_mode; @@ -159,9 +162,9 @@ struct ClimateDeviceRestoreState { * * The entire state of the climate device is encoded in public properties of the base class (current_temperature, * mode etc). These are read-only for the user and rw for integrations. The reason these are public - * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_AUTO) ...` + * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_HEAT_COOL) ...` */ -class Climate : public Nameable { +class Climate : public EntityBase { public: /// Construct a climate device with empty name (will be set later). Climate(); @@ -191,6 +194,7 @@ class Climate : public Nameable { * Away allows climate devices to have two different target temperature configs: * one for normal mode and one for away mode. */ + ESPDEPRECATED("away is deprecated, use preset instead", "v1.20") bool away{false}; /// The active fan mode of the climate device. @@ -242,6 +246,18 @@ class Climate : public Nameable { protected: friend ClimateCall; + /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. + bool set_fan_mode_(ClimateFanMode mode); + + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const std::string &mode); + + /// Set preset. Reset custom preset. Return true if preset has been changed. + bool set_preset_(ClimatePreset preset); + + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const std::string &preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported @@ -267,6 +283,7 @@ class Climate : public Nameable { void save_state_(); uint32_t hash_base() override; + void dump_traits_(const char *tag); CallbackManager state_callback_{}; ESPPreferenceObject rtc_; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 4540208a3f..e46159a750 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -3,103 +3,105 @@ namespace esphome { namespace climate { -const char *climate_mode_to_string(ClimateMode mode) { +const LogString *climate_mode_to_string(ClimateMode mode) { switch (mode) { case CLIMATE_MODE_OFF: - return "OFF"; - case CLIMATE_MODE_AUTO: - return "AUTO"; - case CLIMATE_MODE_COOL: - return "COOL"; - case CLIMATE_MODE_HEAT: - return "HEAT"; - case CLIMATE_MODE_FAN_ONLY: - return "FAN_ONLY"; - case CLIMATE_MODE_DRY: - return "DRY"; + return LOG_STR("OFF"); case CLIMATE_MODE_HEAT_COOL: - return "HEAT_COOL"; + return LOG_STR("HEAT_COOL"); + case CLIMATE_MODE_AUTO: + return LOG_STR("AUTO"); + case CLIMATE_MODE_COOL: + return LOG_STR("COOL"); + case CLIMATE_MODE_HEAT: + return LOG_STR("HEAT"); + case CLIMATE_MODE_FAN_ONLY: + return LOG_STR("FAN_ONLY"); + case CLIMATE_MODE_DRY: + return LOG_STR("DRY"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *climate_action_to_string(ClimateAction action) { +const LogString *climate_action_to_string(ClimateAction action) { switch (action) { case CLIMATE_ACTION_OFF: - return "OFF"; + return LOG_STR("OFF"); case CLIMATE_ACTION_COOLING: - return "COOLING"; + return LOG_STR("COOLING"); case CLIMATE_ACTION_HEATING: - return "HEATING"; + return LOG_STR("HEATING"); case CLIMATE_ACTION_IDLE: - return "IDLE"; + return LOG_STR("IDLE"); case CLIMATE_ACTION_DRYING: - return "DRYING"; + return LOG_STR("DRYING"); case CLIMATE_ACTION_FAN: - return "FAN"; + return LOG_STR("FAN"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *climate_fan_mode_to_string(ClimateFanMode fan_mode) { +const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { switch (fan_mode) { case climate::CLIMATE_FAN_ON: - return "ON"; + return LOG_STR("ON"); case climate::CLIMATE_FAN_OFF: - return "OFF"; + return LOG_STR("OFF"); case climate::CLIMATE_FAN_AUTO: - return "AUTO"; + return LOG_STR("AUTO"); case climate::CLIMATE_FAN_LOW: - return "LOW"; + return LOG_STR("LOW"); case climate::CLIMATE_FAN_MEDIUM: - return "MEDIUM"; + return LOG_STR("MEDIUM"); case climate::CLIMATE_FAN_HIGH: - return "HIGH"; + return LOG_STR("HIGH"); case climate::CLIMATE_FAN_MIDDLE: - return "MIDDLE"; + return LOG_STR("MIDDLE"); case climate::CLIMATE_FAN_FOCUS: - return "FOCUS"; + return LOG_STR("FOCUS"); case climate::CLIMATE_FAN_DIFFUSE: - return "DIFFUSE"; + return LOG_STR("DIFFUSE"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { +const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { switch (swing_mode) { case climate::CLIMATE_SWING_OFF: - return "OFF"; + return LOG_STR("OFF"); case climate::CLIMATE_SWING_BOTH: - return "BOTH"; + return LOG_STR("BOTH"); case climate::CLIMATE_SWING_VERTICAL: - return "VERTICAL"; + return LOG_STR("VERTICAL"); case climate::CLIMATE_SWING_HORIZONTAL: - return "HORIZONTAL"; + return LOG_STR("HORIZONTAL"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *climate_preset_to_string(ClimatePreset preset) { +const LogString *climate_preset_to_string(ClimatePreset preset) { switch (preset) { - case climate::CLIMATE_PRESET_ECO: - return "ECO"; - case climate::CLIMATE_PRESET_AWAY: - return "AWAY"; - case climate::CLIMATE_PRESET_BOOST: - return "BOOST"; - case climate::CLIMATE_PRESET_COMFORT: - return "COMFORT"; + case climate::CLIMATE_PRESET_NONE: + return LOG_STR("NONE"); case climate::CLIMATE_PRESET_HOME: - return "HOME"; + return LOG_STR("HOME"); + case climate::CLIMATE_PRESET_ECO: + return LOG_STR("ECO"); + case climate::CLIMATE_PRESET_AWAY: + return LOG_STR("AWAY"); + case climate::CLIMATE_PRESET_BOOST: + return LOG_STR("BOOST"); + case climate::CLIMATE_PRESET_COMFORT: + return LOG_STR("COMFORT"); case climate::CLIMATE_PRESET_SLEEP: - return "SLEEP"; + return LOG_STR("SLEEP"); case climate::CLIMATE_PRESET_ACTIVITY: - return "ACTIVITY"; + return LOG_STR("ACTIVITY"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index e129fca91d..3e5626919c 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -1,25 +1,29 @@ #pragma once #include +#include "esphome/core/log.h" namespace esphome { namespace climate { /// Enum for all modes a climate device can be in. enum ClimateMode : uint8_t { - /// The climate device is off (not in auto, heat or cool mode) + /// The climate device is off CLIMATE_MODE_OFF = 0, - /// The climate device is set to automatically change the heating/cooling cycle + /// The climate device is set to heat/cool to reach the target temperature. CLIMATE_MODE_HEAT_COOL = 1, - /// The climate device is manually set to cool mode (not in auto mode!) + /// The climate device is set to cool to reach the target temperature CLIMATE_MODE_COOL = 2, - /// The climate device is manually set to heat mode (not in auto mode!) + /// The climate device is set to heat to reach the target temperature CLIMATE_MODE_HEAT = 3, - /// The climate device is manually set to fan only mode + /// The climate device only has the fan enabled, no heating or cooling is taking place CLIMATE_MODE_FAN_ONLY = 4, - /// The climate device is manually set to dry mode + /// The climate device is set to dry/humidity mode CLIMATE_MODE_DRY = 5, - /// The climate device is manually set to heat-cool mode + /** The climate device is adjusting the temperatre dynamically. + * For example, the target temperature can be adjusted based on a schedule, or learned behavior. + * The target temperature can't be adjusted when in this mode. + */ CLIMATE_MODE_AUTO = 6 }; @@ -27,19 +31,18 @@ enum ClimateMode : uint8_t { enum ClimateAction : uint8_t { /// The climate device is off (inactive or no power) CLIMATE_ACTION_OFF = 0, - /// The climate device is actively cooling (usually in cool or auto mode) + /// The climate device is actively cooling CLIMATE_ACTION_COOLING = 2, - /// The climate device is actively heating (usually in heat or auto mode) + /// The climate device is actively heating CLIMATE_ACTION_HEATING = 3, /// The climate device is idle (monitoring climate but no action needed) CLIMATE_ACTION_IDLE = 4, - /// The climate device is drying (either mode DRY or AUTO) + /// The climate device is drying CLIMATE_ACTION_DRYING = 5, - /// The climate device is in fan only mode (either mode FAN_ONLY or AUTO) + /// The climate device is in fan only mode CLIMATE_ACTION_FAN = 6, }; -/// Enum for all modes a climate fan can be in enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -75,36 +78,38 @@ enum ClimateSwingMode : uint8_t { /// Enum for all modes a climate swing can be in enum ClimatePreset : uint8_t { - /// Preset is set to ECO - CLIMATE_PRESET_ECO = 0, - /// Preset is set to AWAY - CLIMATE_PRESET_AWAY = 1, - /// Preset is set to BOOST - CLIMATE_PRESET_BOOST = 2, - /// Preset is set to COMFORT - CLIMATE_PRESET_COMFORT = 3, - /// Preset is set to HOME - CLIMATE_PRESET_HOME = 4, - /// Preset is set to SLEEP - CLIMATE_PRESET_SLEEP = 5, - /// Preset is set to ACTIVITY - CLIMATE_PRESET_ACTIVITY = 6, + /// No preset is active + CLIMATE_PRESET_NONE = 0, + /// Device is in home preset + CLIMATE_PRESET_HOME = 1, + /// Device is in away preset + CLIMATE_PRESET_AWAY = 2, + /// Device is in boost preset + CLIMATE_PRESET_BOOST = 3, + /// Device is in comfort preset + CLIMATE_PRESET_COMFORT = 4, + /// Device is running an energy-saving preset + CLIMATE_PRESET_ECO = 5, + /// Device is prepared for sleep + CLIMATE_PRESET_SLEEP = 6, + /// Device is reacting to activity (e.g., movement sensors) + CLIMATE_PRESET_ACTIVITY = 7, }; /// Convert the given ClimateMode to a human-readable string. -const char *climate_mode_to_string(ClimateMode mode); +const LogString *climate_mode_to_string(ClimateMode mode); /// Convert the given ClimateAction to a human-readable string. -const char *climate_action_to_string(ClimateAction action); +const LogString *climate_action_to_string(ClimateAction action); /// Convert the given ClimateFanMode to a human-readable string. -const char *climate_fan_mode_to_string(ClimateFanMode mode); +const LogString *climate_fan_mode_to_string(ClimateFanMode mode); /// Convert the given ClimateSwingMode to a human-readable string. -const char *climate_swing_mode_to_string(ClimateSwingMode mode); +const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); /// Convert the given ClimateSwingMode to a human-readable string. -const char *climate_preset_to_string(ClimatePreset preset); +const LogString *climate_preset_to_string(ClimatePreset preset); } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index eda4722fcb..16c9cd05be 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,53 +1,9 @@ #include "climate_traits.h" -#include "esphome/core/log.h" +#include namespace esphome { namespace climate { -bool ClimateTraits::supports_mode(ClimateMode mode) const { - switch (mode) { - case CLIMATE_MODE_OFF: - return true; - case CLIMATE_MODE_AUTO: - return this->supports_auto_mode_; - case CLIMATE_MODE_COOL: - return this->supports_cool_mode_; - case CLIMATE_MODE_HEAT: - return this->supports_heat_mode_; - case CLIMATE_MODE_FAN_ONLY: - return this->supports_fan_only_mode_; - case CLIMATE_MODE_DRY: - return this->supports_dry_mode_; - default: - return false; - } -} -bool ClimateTraits::get_supports_current_temperature() const { return supports_current_temperature_; } -void ClimateTraits::set_supports_current_temperature(bool supports_current_temperature) { - supports_current_temperature_ = supports_current_temperature; -} -bool ClimateTraits::get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } -void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { - supports_two_point_target_temperature_ = supports_two_point_target_temperature; -} -void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } -void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } -void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } -void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { - supports_fan_only_mode_ = supports_fan_only_mode; -} -void ClimateTraits::set_supports_dry_mode(bool supports_dry_mode) { supports_dry_mode_ = supports_dry_mode; } -void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } -void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } -float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } -void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { - visual_min_temperature_ = visual_min_temperature; -} -float ClimateTraits::get_visual_max_temperature() const { return visual_max_temperature_; } -void ClimateTraits::set_visual_max_temperature(float visual_max_temperature) { - visual_max_temperature_ = visual_max_temperature; -} -float ClimateTraits::get_visual_temperature_step() const { return visual_temperature_step_; } int8_t ClimateTraits::get_temperature_accuracy_decimals() const { // use printf %g to find number of digits based on temperature step char buf[32]; @@ -59,160 +15,6 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { return str.length() - dot_pos - 1; } -void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } -bool ClimateTraits::get_supports_away() const { return supports_away_; } -bool ClimateTraits::get_supports_action() const { return supports_action_; } -void ClimateTraits::set_supports_fan_mode_on(bool supports_fan_mode_on) { - this->supports_fan_mode_on_ = supports_fan_mode_on; -} -void ClimateTraits::set_supports_fan_mode_off(bool supports_fan_mode_off) { - this->supports_fan_mode_off_ = supports_fan_mode_off; -} -void ClimateTraits::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { - this->supports_fan_mode_auto_ = supports_fan_mode_auto; -} -void ClimateTraits::set_supports_fan_mode_low(bool supports_fan_mode_low) { - this->supports_fan_mode_low_ = supports_fan_mode_low; -} -void ClimateTraits::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { - this->supports_fan_mode_medium_ = supports_fan_mode_medium; -} -void ClimateTraits::set_supports_fan_mode_high(bool supports_fan_mode_high) { - this->supports_fan_mode_high_ = supports_fan_mode_high; -} -void ClimateTraits::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { - this->supports_fan_mode_middle_ = supports_fan_mode_middle; -} -void ClimateTraits::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { - this->supports_fan_mode_focus_ = supports_fan_mode_focus; -} -void ClimateTraits::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { - this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; -} -bool ClimateTraits::supports_fan_mode(ClimateFanMode fan_mode) const { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return this->supports_fan_mode_on_; - case climate::CLIMATE_FAN_OFF: - return this->supports_fan_mode_off_; - case climate::CLIMATE_FAN_AUTO: - return this->supports_fan_mode_auto_; - case climate::CLIMATE_FAN_LOW: - return this->supports_fan_mode_low_; - case climate::CLIMATE_FAN_MEDIUM: - return this->supports_fan_mode_medium_; - case climate::CLIMATE_FAN_HIGH: - return this->supports_fan_mode_high_; - case climate::CLIMATE_FAN_MIDDLE: - return this->supports_fan_mode_middle_; - case climate::CLIMATE_FAN_FOCUS: - return this->supports_fan_mode_focus_; - case climate::CLIMATE_FAN_DIFFUSE: - return this->supports_fan_mode_diffuse_; - default: - return false; - } -} -bool ClimateTraits::get_supports_fan_modes() const { - return 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_; -} -void ClimateTraits::set_supported_custom_fan_modes(std::vector &supported_custom_fan_modes) { - this->supported_custom_fan_modes_ = supported_custom_fan_modes; -} -const std::vector ClimateTraits::get_supported_custom_fan_modes() const { - return this->supported_custom_fan_modes_; -} -bool ClimateTraits::supports_custom_fan_mode(std::string &custom_fan_mode) const { - return std::count(this->supported_custom_fan_modes_.begin(), this->supported_custom_fan_modes_.end(), - custom_fan_mode); -} -bool ClimateTraits::supports_preset(ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - return this->supports_preset_eco_; - case climate::CLIMATE_PRESET_AWAY: - return this->supports_preset_away_; - case climate::CLIMATE_PRESET_BOOST: - return this->supports_preset_boost_; - case climate::CLIMATE_PRESET_COMFORT: - return this->supports_preset_comfort_; - case climate::CLIMATE_PRESET_HOME: - return this->supports_preset_home_; - case climate::CLIMATE_PRESET_SLEEP: - return this->supports_preset_sleep_; - case climate::CLIMATE_PRESET_ACTIVITY: - return this->supports_preset_activity_; - default: - return false; - } -} -void ClimateTraits::set_supports_preset_eco(bool supports_preset_eco) { - this->supports_preset_eco_ = supports_preset_eco; -} -void ClimateTraits::set_supports_preset_away(bool supports_preset_away) { - this->supports_preset_away_ = supports_preset_away; -} -void ClimateTraits::set_supports_preset_boost(bool supports_preset_boost) { - this->supports_preset_boost_ = supports_preset_boost; -} -void ClimateTraits::set_supports_preset_comfort(bool supports_preset_comfort) { - this->supports_preset_comfort_ = supports_preset_comfort; -} -void ClimateTraits::set_supports_preset_home(bool supports_preset_home) { - this->supports_preset_home_ = supports_preset_home; -} -void ClimateTraits::set_supports_preset_sleep(bool supports_preset_sleep) { - this->supports_preset_sleep_ = supports_preset_sleep; -} -void ClimateTraits::set_supports_preset_activity(bool supports_preset_activity) { - this->supports_preset_activity_ = supports_preset_activity; -} -bool ClimateTraits::get_supports_presets() const { - return this->supports_preset_eco_ || this->supports_preset_away_ || this->supports_preset_boost_ || - this->supports_preset_comfort_ || this->supports_preset_home_ || this->supports_preset_sleep_ || - this->supports_preset_activity_; -} -void ClimateTraits::set_supported_custom_presets(std::vector &supported_custom_presets) { - this->supported_custom_presets_ = supported_custom_presets; -} -const std::vector ClimateTraits::get_supported_custom_presets() const { - return this->supported_custom_presets_; -} -bool ClimateTraits::supports_custom_preset(std::string &custom_preset) const { - return std::count(this->supported_custom_presets_.begin(), this->supported_custom_presets_.end(), custom_preset); -} -void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) { - this->supports_swing_mode_off_ = supports_swing_mode_off; -} -void ClimateTraits::set_supports_swing_mode_both(bool supports_swing_mode_both) { - this->supports_swing_mode_both_ = supports_swing_mode_both; -} -void ClimateTraits::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { - this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; -} -void ClimateTraits::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { - this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; -} -bool ClimateTraits::supports_swing_mode(ClimateSwingMode swing_mode) const { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return this->supports_swing_mode_off_; - case climate::CLIMATE_SWING_BOTH: - return this->supports_swing_mode_both_; - case climate::CLIMATE_SWING_VERTICAL: - return this->supports_swing_mode_vertical_; - case climate::CLIMATE_SWING_HORIZONTAL: - return this->supports_swing_mode_horizontal_; - default: - return false; - } -} -bool ClimateTraits::get_supports_swing_modes() const { - return this->supports_swing_mode_off_ || this->supports_swing_mode_both_ || supports_swing_mode_vertical_ || - supports_swing_mode_horizontal_; -} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f0a48ca308..903ce085d8 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,6 +2,7 @@ #include "esphome/core/helpers.h" #include "climate_mode.h" +#include namespace esphome { namespace climate { @@ -24,8 +25,6 @@ namespace climate { * - heat mode (increases current temperature) * - dry mode (removes humidity from air) * - fan mode (only turns on fan) - * - supports away - away mode means that the climate device supports two different - * target temperature settings: one target temp setting for "away" mode and one for non-away mode. * - supports action - if the climate device supports reporting the active * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: @@ -41,93 +40,149 @@ namespace climate { */ class ClimateTraits { public: - bool get_supports_current_temperature() const; - void set_supports_current_temperature(bool supports_current_temperature); - bool get_supports_two_point_target_temperature() const; - void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature); - void set_supports_auto_mode(bool supports_auto_mode); - void set_supports_cool_mode(bool supports_cool_mode); - void set_supports_heat_mode(bool supports_heat_mode); - void set_supports_fan_only_mode(bool supports_fan_only_mode); - void set_supports_dry_mode(bool supports_dry_mode); - void set_supports_away(bool supports_away); - bool get_supports_away() const; - void set_supports_action(bool supports_action); - bool get_supports_action() const; - bool supports_mode(ClimateMode mode) const; - void set_supports_fan_mode_on(bool supports_fan_mode_on); - void set_supports_fan_mode_off(bool supports_fan_mode_off); - void set_supports_fan_mode_auto(bool supports_fan_mode_auto); - void set_supports_fan_mode_low(bool supports_fan_mode_low); - void set_supports_fan_mode_medium(bool supports_fan_mode_medium); - void set_supports_fan_mode_high(bool supports_fan_mode_high); - void set_supports_fan_mode_middle(bool supports_fan_mode_middle); - void set_supports_fan_mode_focus(bool supports_fan_mode_focus); - void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); - bool supports_fan_mode(ClimateFanMode fan_mode) const; - bool get_supports_fan_modes() const; - void set_supported_custom_fan_modes(std::vector &supported_custom_fan_modes); - const std::vector get_supported_custom_fan_modes() const; - bool supports_custom_fan_mode(std::string &custom_fan_mode) const; - bool supports_preset(ClimatePreset preset) const; - void set_supports_preset_eco(bool supports_preset_eco); - void set_supports_preset_away(bool supports_preset_away); - void set_supports_preset_boost(bool supports_preset_boost); - void set_supports_preset_comfort(bool supports_preset_comfort); - void set_supports_preset_home(bool supports_preset_home); - void set_supports_preset_sleep(bool supports_preset_sleep); - void set_supports_preset_activity(bool supports_preset_activity); - bool get_supports_presets() const; - void set_supported_custom_presets(std::vector &supported_custom_presets); - const std::vector get_supported_custom_presets() const; - bool supports_custom_preset(std::string &custom_preset) const; - void set_supports_swing_mode_off(bool supports_swing_mode_off); - void set_supports_swing_mode_both(bool supports_swing_mode_both); - void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); - void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); - bool supports_swing_mode(ClimateSwingMode swing_mode) const; - bool get_supports_swing_modes() const; + bool get_supports_current_temperature() const { return supports_current_temperature_; } + void set_supports_current_temperature(bool supports_current_temperature) { + supports_current_temperature_ = supports_current_temperature; + } + 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; + } + 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") + void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") + void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") + void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") + void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") + void set_supports_fan_only_mode(bool supports_fan_only_mode) { + set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode); + } + 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); } + const std::set get_supported_modes() const { return supported_modes_; } - float get_visual_min_temperature() const; - void set_visual_min_temperature(float visual_min_temperature); - float get_visual_max_temperature() const; - void set_visual_max_temperature(float visual_max_temperature); - float get_visual_temperature_step() const; + void set_supports_action(bool supports_action) { supports_action_ = supports_action; } + bool get_supports_action() const { return supports_action_; } + + void set_supported_fan_modes(std::set modes) { supported_fan_modes_ = std::move(modes); } + void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") + 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(); } + 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); + } + const std::set &get_supported_custom_fan_modes() const { return supported_custom_fan_modes_; } + bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + return supported_custom_fan_modes_.count(custom_fan_mode); + } + + void set_supported_presets(std::set presets) { supported_presets_ = std::move(presets); } + void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } + void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } + bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } + bool get_supports_presets() const { return !supported_presets_.empty(); } + const std::set &get_supported_presets() const { return supported_presets_; } + + void set_supported_custom_presets(std::set supported_custom_presets) { + supported_custom_presets_ = std::move(supported_custom_presets); + } + const std::set &get_supported_custom_presets() const { return supported_custom_presets_; } + bool supports_custom_preset(const std::string &custom_preset) const { + return supported_custom_presets_.count(custom_preset); + } + ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20") + void set_supports_away(bool supports) { + if (supports) { + supported_presets_.insert(CLIMATE_PRESET_AWAY); + supported_presets_.insert(CLIMATE_PRESET_HOME); + } + } + ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20") + bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); } + + void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); } + void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") + void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") + void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") + void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") + void set_supports_swing_mode_horizontal(bool supported) { + set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); + } + 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(); } + const std::set get_supported_swing_modes() { 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; } + float get_visual_max_temperature() const { return visual_max_temperature_; } + void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } + float get_visual_temperature_step() const { return visual_temperature_step_; } int8_t get_temperature_accuracy_decimals() const; - void set_visual_temperature_step(float temperature_step); + void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } protected: + void set_mode_support_(climate::ClimateMode mode, bool supported) { + if (supported) { + supported_modes_.insert(mode); + } else { + supported_modes_.erase(mode); + } + } + void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { + if (supported) { + supported_fan_modes_.insert(mode); + } else { + supported_fan_modes_.erase(mode); + } + } + void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { + if (supported) { + supported_swing_modes_.insert(mode); + } else { + supported_swing_modes_.erase(mode); + } + } + bool supports_current_temperature_{false}; bool supports_two_point_target_temperature_{false}; - bool supports_auto_mode_{false}; - bool supports_cool_mode_{false}; - bool supports_heat_mode_{false}; - bool supports_fan_only_mode_{false}; - bool supports_dry_mode_{false}; - bool supports_away_{false}; + std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; - bool supports_fan_mode_on_{false}; - bool supports_fan_mode_off_{false}; - bool supports_fan_mode_auto_{false}; - bool supports_fan_mode_low_{false}; - bool supports_fan_mode_medium_{false}; - bool supports_fan_mode_high_{false}; - bool supports_fan_mode_middle_{false}; - bool supports_fan_mode_focus_{false}; - bool supports_fan_mode_diffuse_{false}; - bool supports_swing_mode_off_{false}; - bool supports_swing_mode_both_{false}; - bool supports_swing_mode_vertical_{false}; - bool supports_swing_mode_horizontal_{false}; - bool supports_preset_eco_{false}; - bool supports_preset_away_{false}; - bool supports_preset_boost_{false}; - bool supports_preset_comfort_{false}; - bool supports_preset_home_{false}; - bool supports_preset_sleep_{false}; - bool supports_preset_activity_{false}; - std::vector supported_custom_fan_modes_; - std::vector supported_custom_presets_; + std::set supported_fan_modes_; + std::set supported_swing_modes_; + std::set supported_presets_; + std::set supported_custom_fan_modes_; + std::set supported_custom_presets_; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 3c9d118736..b47d9b0141 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -9,63 +9,22 @@ static const char *const TAG = "climate_ir"; climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_dry_mode(this->supports_dry_); - traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_dry_) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (supports_fan_only_) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); + traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); - for (auto fan_mode : this->fan_modes_) { - switch (fan_mode) { - case climate::CLIMATE_FAN_AUTO: - traits.set_supports_fan_mode_auto(true); - break; - case climate::CLIMATE_FAN_DIFFUSE: - traits.set_supports_fan_mode_diffuse(true); - break; - case climate::CLIMATE_FAN_FOCUS: - traits.set_supports_fan_mode_focus(true); - break; - case climate::CLIMATE_FAN_HIGH: - traits.set_supports_fan_mode_high(true); - break; - case climate::CLIMATE_FAN_LOW: - traits.set_supports_fan_mode_low(true); - break; - case climate::CLIMATE_FAN_MEDIUM: - traits.set_supports_fan_mode_medium(true); - break; - case climate::CLIMATE_FAN_MIDDLE: - traits.set_supports_fan_mode_middle(true); - break; - case climate::CLIMATE_FAN_OFF: - traits.set_supports_fan_mode_off(true); - break; - case climate::CLIMATE_FAN_ON: - traits.set_supports_fan_mode_on(true); - break; - } - } - for (auto swing_mode : this->swing_modes_) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - traits.set_supports_swing_mode_off(true); - break; - case climate::CLIMATE_SWING_BOTH: - traits.set_supports_swing_mode_both(true); - break; - case climate::CLIMATE_SWING_VERTICAL: - traits.set_supports_swing_mode_vertical(true); - break; - case climate::CLIMATE_SWING_HORIZONTAL: - traits.set_supports_swing_mode_horizontal(true); - break; - } - } + traits.set_supported_fan_modes(fan_modes_); + traits.set_supported_swing_modes(swing_modes_); return traits; } @@ -93,7 +52,7 @@ void ClimateIR::setup() { this->swing_mode = climate::CLIMATE_SWING_OFF; } // Never send nan to HA - if (isnan(this->target_temperature)) + if (std::isnan(this->target_temperature)) this->target_temperature = 24; } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 0914f730cf..677021da29 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -21,9 +21,8 @@ namespace climate_ir { class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, - std::vector fan_modes = {}, - std::vector swing_modes = {}) { + bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, + std::set swing_modes = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; @@ -60,8 +59,8 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::vector fan_modes_ = {}; - std::vector swing_modes_ = {}; + std::set fan_modes_ = {}; + std::set swing_modes_ = {}; 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 892560db8d..cbb1f7699b 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -39,7 +39,7 @@ void LgIrClimate::transmit_state() { send_swing_cmd_ = false; remote_state |= COMMAND_SWING; } else { - if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) { + 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; @@ -52,7 +52,7 @@ void LgIrClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state |= COMMAND_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state |= COMMAND_AUTO; break; case climate::CLIMATE_MODE_DRY: @@ -89,12 +89,12 @@ void LgIrClimate::transmit_state() { } } - if (this->mode == climate::CLIMATE_MODE_AUTO) { + 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)); + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); remote_state |= ((temp - 15) << TEMP_SHIFT); } } @@ -128,7 +128,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { 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_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; } if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { @@ -138,7 +138,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { 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_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) { @@ -152,7 +152,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; // Fan Speed - if (this->mode == climate::CLIMATE_MODE_AUTO) { + 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) { diff --git a/esphome/components/color_temperature/__init__.py b/esphome/components/color_temperature/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/color_temperature/ct_light_output.h b/esphome/components/color_temperature/ct_light_output.h new file mode 100644 index 0000000000..4ff86c8b80 --- /dev/null +++ b/esphome/components/color_temperature/ct_light_output.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/components/light/light_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace color_temperature { + +class CTLightOutput : public light::LightOutput { + public: + void set_color_temperature(output::FloatOutput *color_temperature) { color_temperature_ = color_temperature; } + void set_brightness(output::FloatOutput *brightness) { brightness_ = brightness; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); + traits.set_min_mireds(this->cold_white_temperature_); + traits.set_max_mireds(this->warm_white_temperature_); + return traits; + } + void write_state(light::LightState *state) override { + float color_temperature, brightness; + state->current_values_as_ct(&color_temperature, &brightness); + this->color_temperature_->set_level(color_temperature); + this->brightness_->set_level(brightness); + } + + protected: + output::FloatOutput *color_temperature_; + output::FloatOutput *brightness_; + float cold_white_temperature_; + float warm_white_temperature_; +}; + +} // namespace color_temperature +} // namespace esphome diff --git a/esphome/components/color_temperature/light.py b/esphome/components/color_temperature/light.py new file mode 100644 index 0000000000..3e7a0e73ae --- /dev/null +++ b/esphome/components/color_temperature/light.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light, output +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_TEMPERATURE, + CONF_OUTPUT_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) + +CODEOWNERS = ["@jesserockz"] + +color_temperature_ns = cg.esphome_ns.namespace("color_temperature") +CTLightOutput = color_temperature_ns.class_("CTLightOutput", light.LightOutput) + +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CTLightOutput), + cv.Required(CONF_COLOR_TEMPERATURE): cv.use_id(output.FloatOutput), + cv.Required(CONF_BRIGHTNESS): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + } + ), + light.validate_color_temperature_channels, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + color_temperature = await cg.get_variable(config[CONF_COLOR_TEMPERATURE]) + cg.add(var.set_color_temperature(color_temperature)) + + brightness = await cg.get_variable(config[CONF_BRIGHTNESS]) + cg.add(var.set_brightness(brightness)) + + cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index b28cee9245..c9145e4ecf 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -70,7 +70,7 @@ void CoolixClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state |= COOLIX_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state |= COOLIX_AUTO; break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -84,12 +84,12 @@ void CoolixClimate::transmit_state() { } if (this->mode != climate::CLIMATE_MODE_OFF) { if (this->mode != climate::CLIMATE_MODE_FAN_ONLY) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); + auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; } else { remote_state |= COOLIX_FAN_TEMP_CODE; } - if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) { + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_DRY) { this->fan_mode = climate::CLIMATE_FAN_AUTO; remote_state |= COOLIX_FAN_MODE_AUTO_DRY; } else { @@ -197,7 +197,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) this->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) this->mode = climate::CLIMATE_MODE_DRY; @@ -207,7 +207,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO || + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_DRY) this->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 4a7266303d..0fd27f3f27 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -5,16 +5,20 @@ from esphome.automation import maybe_simple_id, Condition from esphome.components import mqtt from esphome.const import ( CONF_ID, - CONF_INTERNAL, CONF_DEVICE_CLASS, CONF_STATE, CONF_POSITION, + CONF_POSITION_COMMAND_TOPIC, + CONF_POSITION_STATE_TOPIC, CONF_TILT, + CONF_TILT_COMMAND_TOPIC, + CONF_TILT_STATE_TOPIC, CONF_STOP, CONF_MQTT_ID, - CONF_NAME, + CONF_TRIGGER_ID, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -35,7 +39,7 @@ DEVICE_CLASSES = [ cover_ns = cg.esphome_ns.namespace("cover") -Cover = cover_ns.class_("Cover", cg.Nameable) +Cover = cover_ns.class_("Cover", cg.EntityBase) COVER_OPEN = cover_ns.COVER_OPEN COVER_CLOSED = cover_ns.COVER_CLOSED @@ -58,32 +62,84 @@ validate_cover_operation = cv.enum(COVER_OPERATIONS, upper=True) OpenAction = cover_ns.class_("OpenAction", automation.Action) CloseAction = cover_ns.class_("CloseAction", automation.Action) StopAction = cover_ns.class_("StopAction", automation.Action) +ToggleAction = cover_ns.class_("ToggleAction", automation.Action) ControlAction = cover_ns.class_("ControlAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) -COVER_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +# Triggers +CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverClosedTrigger = cover_ns.class_( + "CoverClosedTrigger", automation.Trigger.template() +) + +CONF_ON_OPEN = "on_open" +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), - # TODO: MQTT topic options + 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): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) + await setup_entity(var, config) + if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + 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 CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_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])) + async def register_cover(var, config): if not CORE.has_id(config[CONF_ID]): @@ -117,6 +173,12 @@ async def cover_stop_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) +def cover_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) + + COVER_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(Cover), diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 0092f987f2..6406ba52cb 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->open(); } + void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); } protected: Cover *cover_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->close(); } + void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); } protected: Cover *cover_; @@ -31,7 +31,17 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->stop(); } + void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); } + + protected: + Cover *cover_; +}; + +template class ToggleAction : public Action { + public: + explicit ToggleAction(Cover *cover) : cover_(cover) {} + + void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); } protected: Cover *cover_; @@ -89,6 +99,7 @@ template class CoverIsOpenCondition : public Condition { protected: Cover *cover_; }; + template class CoverIsClosedCondition : public Condition { public: CoverIsClosedCondition(Cover *cover) : cover_(cover) {} @@ -98,5 +109,27 @@ template class CoverIsClosedCondition : public Condition Cover *cover_; }; +class CoverOpenTrigger : public Trigger<> { + public: + CoverOpenTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->is_fully_open()) { + this->trigger(); + } + }); + } +}; + +class CoverClosedTrigger : public Trigger<> { + public: + CoverClosedTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->is_fully_closed()) { + this->trigger(); + } + }); + } +}; + } // namespace cover } // namespace esphome diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 2b4452f5b7..a8d3d691a4 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -31,7 +31,7 @@ const char *cover_operation_to_str(CoverOperation op) { } } -Cover::Cover(const std::string &name) : Nameable(name), position{COVER_OPEN} {} +Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {} uint32_t Cover::hash_base() { return 1727367479UL; } @@ -43,6 +43,8 @@ CoverCall &CoverCall::set_command(const char *command) { 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); } @@ -60,6 +62,10 @@ CoverCall &CoverCall::set_command_stop() { this->stop_ = true; return *this; } +CoverCall &CoverCall::set_command_toggle() { + this->toggle_ = true; + return *this; +} CoverCall &CoverCall::set_position(float position) { this->position_ = position; return *this; @@ -85,10 +91,14 @@ void CoverCall::perform() { if (this->tilt_.has_value()) { ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); } + if (this->toggle_.has_value()) { + ESP_LOGD(TAG, " Command: TOGGLE"); + } this->parent_->control(*this); } const optional &CoverCall::get_position() const { return this->position_; } const optional &CoverCall::get_tilt() const { return this->tilt_; } +const optional &CoverCall::get_toggle() const { return this->toggle_; } void CoverCall::validate_() { auto traits = this->parent_->get_traits(); if (this->position_.has_value()) { @@ -111,6 +121,12 @@ void CoverCall::validate_() { this->tilt_ = clamp(tilt, 0.0f, 1.0f); } } + if (this->toggle_.has_value()) { + if (!traits.get_supports_toggle()) { + ESP_LOGW(TAG, "'%s' - This cover 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 cover!"); @@ -120,6 +136,10 @@ void CoverCall::validate_() { ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!"); this->tilt_.reset(); } + if (this->toggle_.has_value()) { + ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!"); + this->toggle_.reset(); + } } } CoverCall &CoverCall::set_stop(bool stop) { @@ -180,7 +200,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 1af4f9cbea..a67f8d2393 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "cover_traits.h" @@ -13,7 +14,7 @@ const extern float COVER_CLOSED; #define LOG_COVER(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + 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); \ @@ -29,7 +30,7 @@ class CoverCall { public: CoverCall(Cover *parent); - /// Set the command as a string, "STOP", "OPEN", "CLOSE". + /// Set the command as a string, "STOP", "OPEN", "CLOSE", "TOGGLE". CoverCall &set_command(const char *command); /// Set the command to open the cover. CoverCall &set_command_open(); @@ -37,6 +38,8 @@ class CoverCall { CoverCall &set_command_close(); /// Set the command to stop the cover. CoverCall &set_command_stop(); + /// Set the command to toggle the cover. + CoverCall &set_command_toggle(); /// Set the call to a certain target position. CoverCall &set_position(float position); /// Set the call to a certain target tilt. @@ -50,6 +53,7 @@ class CoverCall { const optional &get_position() const; bool get_stop() const; const optional &get_tilt() const; + const optional &get_toggle() const; protected: void validate_(); @@ -58,6 +62,7 @@ class CoverCall { bool stop_{false}; optional position_{}; optional tilt_{}; + optional toggle_{}; }; /// Struct used to store the restored state of a cover @@ -103,22 +108,19 @@ const char *cover_operation_to_str(CoverOperation op); * to control all values of the cover. Also implement get_traits() to return what operations * the cover supports. */ -class Cover : public Nameable { +class Cover : public EntityBase { public: explicit Cover(); explicit Cover(const std::string &name); /// The current operation of the cover (idle, opening, closing). CoverOperation current_operation{COVER_OPERATION_IDLE}; - union { - /** The position of the cover from 0.0 (fully closed) to 1.0 (fully open). - * - * For binary covers this is always equals to 0.0 or 1.0 (see also COVER_OPEN and - * COVER_CLOSED constants). - */ - float position; - ESPDEPRECATED(".state is deprecated, please use .position instead") float state; - }; + /** The position of the cover from 0.0 (fully closed) to 1.0 (fully open). + * + * For binary covers this is always equals to 0.0 or 1.0 (see also COVER_OPEN and + * COVER_CLOSED constants). + */ + float position; /// The current tilt value of the cover from 0.0 to 1.0. float tilt{COVER_OPEN}; @@ -128,16 +130,19 @@ class Cover : public Nameable { * * 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") 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") void close(); /** Stop the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") void stop(); void add_on_state_callback(std::function &&f); diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index 2df4a0738e..fb30883f77 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -13,11 +13,14 @@ class CoverTraits { void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; } bool get_supports_tilt() const { return this->supports_tilt_; } void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; } + bool get_supports_toggle() const { return this->supports_toggle_; } + void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; } protected: bool is_assumed_state_{false}; bool supports_position_{false}; bool supports_tilt_{false}; + bool supports_toggle_{false}; }; } // namespace cover diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index efb1d1d426..82df881bfc 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - ICON_EMPTY, DEVICE_CLASS_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_VOLTAGE, @@ -80,13 +79,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLTAGE_HPF, default=True): cv.boolean, cv.Optional(CONF_PULSE_ENERGY, default=10.0): validate_energy, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, ), } ) diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 98cf4da96d..1c8efc4f72 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -28,23 +27,31 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(CSE7766Component), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + 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_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + 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(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "cse7766", baud_rate=4800, require_rx=True +) async def to_code(config): @@ -64,9 +71,3 @@ async def to_code(config): conf = config[CONF_POWER] sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) - - -def validate(config, item_config): - uart.validate_device( - "cse7766", config, item_config, baud_rate=4800, require_tx=False - ) diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index c27134d6ac..51b0f1318c 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -8,18 +8,6 @@ namespace ct_clamp { static const char *const TAG = "ct_clamp"; -void CTClampSensor::setup() { - this->is_calibrating_offset_ = true; - this->high_freq_.start(); - this->set_timeout("calibrate_offset", this->sample_duration_, [this]() { - this->high_freq_.stop(); - this->is_calibrating_offset_ = false; - if (this->num_samples_ != 0) { - this->offset_ = this->sample_sum_ / this->num_samples_; - } - }); -} - void CTClampSensor::dump_config() { LOG_SENSOR("", "CT Clamp Sensor", this); ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); @@ -27,9 +15,6 @@ void CTClampSensor::dump_config() { } void CTClampSensor::update() { - if (this->is_calibrating_offset_) - return; - // Update only starts the sampling phase, in loop() the actual sampling is happening. // Request a high loop() execution interval during sampling phase. @@ -46,44 +31,39 @@ void CTClampSensor::update() { return; } - float raw = this->sample_sum_ / this->num_samples_; - float irms = std::sqrt(raw); - ESP_LOGD(TAG, "'%s' - Raw Value: %.2fA", this->name_.c_str(), irms); - this->publish_state(irms); + const float rms_ac_dc_squared = this->sample_squared_sum_ / this->num_samples_; + const float rms_dc = this->sample_sum_ / this->num_samples_; + const float rms_ac = std::sqrt(rms_ac_dc_squared - rms_dc * rms_dc); + 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_); + this->publish_state(rms_ac); }); // Set sampling values - this->is_sampling_ = true; + this->last_value_ = 0.0; this->num_samples_ = 0; this->sample_sum_ = 0.0f; + this->sample_squared_sum_ = 0.0f; + this->is_sampling_ = true; } void CTClampSensor::loop() { - if (!this->is_sampling_ && !this->is_calibrating_offset_) + if (!this->is_sampling_) return; // Perform a single sample float value = this->source_->sample(); - if (isnan(value)) + if (std::isnan(value)) return; - if (this->is_calibrating_offset_) { - this->sample_sum_ += value; - this->num_samples_++; + // Assuming a sine wave, avoid requesting values faster than the ADC can provide them + if (this->last_value_ == value) return; - } + this->last_value_ = value; - // Adjust DC offset via low pass filter (exponential moving average) - const float alpha = 0.001f; - this->offset_ = this->offset_ * (1 - alpha) + value * alpha; - - // Filtered value centered around the mid-point (0V) - float filtered = value - this->offset_; - - // IRMS is sqrt(∑v_i²) - float sq = filtered * filtered; - this->sample_sum_ += sq; this->num_samples_++; + this->sample_sum_ += value; + this->sample_squared_sum_ += value * value; } } // namespace ct_clamp diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h index c709f6718b..db4dc1ea57 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.h +++ b/esphome/components/ct_clamp/ct_clamp_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" @@ -10,7 +10,6 @@ namespace ct_clamp { class CTClampSensor : public sensor::Sensor, public PollingComponent { public: - void setup() override; void update() override; void loop() override; void dump_config() override; @@ -35,17 +34,20 @@ class CTClampSensor : public sensor::Sensor, public PollingComponent { * * Diagram: https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/interface-with-arduino * - * This is automatically calculated with an exponential moving average/digital low pass filter. - * - * 0.5 is a good initial approximation to start with for most ESP8266 setups. + * The current clamp only measures AC, so any DC component is an unwanted artifact from the + * sampling circuit. The AC component is essentially the same as the calculating the Standard-Deviation, + * which can be done by cumulating 3 values per sample: + * 1) Number of samples + * 2) Sum of samples + * 3) Sum of sample squared + * https://en.wikipedia.org/wiki/Root_mean_square */ - float offset_ = 0.5f; + float last_value_ = 0.0f; float sample_sum_ = 0.0f; + float sample_squared_sum_ = 0.0f; uint32_t num_samples_ = 0; bool is_sampling_ = false; - /// Calibrate offset value once at boot - bool is_calibrating_offset_ = false; }; } // namespace ct_clamp diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index e44d46e7f4..049905d0a7 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_SENSOR, CONF_ID, DEVICE_CLASS_CURRENT, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, ) @@ -20,7 +19,10 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/current_based/__init__.py b/esphome/components/current_based/__init__.py new file mode 100644 index 0000000000..e7f41154b7 --- /dev/null +++ b/esphome/components/current_based/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@djwmarcx"] diff --git a/esphome/components/current_based/cover.py b/esphome/components/current_based/cover.py new file mode 100644 index 0000000000..eb77a90aff --- /dev/null +++ b/esphome/components/current_based/cover.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import sensor, cover +from esphome.const import ( + CONF_CLOSE_ACTION, + CONF_CLOSE_DURATION, + CONF_ID, + CONF_OPEN_ACTION, + CONF_OPEN_DURATION, + CONF_STOP_ACTION, + CONF_MAX_DURATION, +) + + +CONF_OPEN_SENSOR = "open_sensor" +CONF_OPEN_MOVING_CURRENT_THRESHOLD = "open_moving_current_threshold" +CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD = "open_obstacle_current_threshold" + +CONF_CLOSE_SENSOR = "close_sensor" +CONF_CLOSE_MOVING_CURRENT_THRESHOLD = "close_moving_current_threshold" +CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD = "close_obstacle_current_threshold" + +CONF_OBSTACLE_ROLLBACK = "obstacle_rollback" +CONF_MALFUNCTION_DETECTION = "malfunction_detection" +CONF_MALFUNCTION_ACTION = "malfunction_action" +CONF_START_SENSING_DELAY = "start_sensing_delay" + +current_based_ns = cg.esphome_ns.namespace("current_based") +CurrentBasedCover = current_based_ns.class_( + "CurrentBasedCover", cover.Cover, cg.Component +) + +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CurrentBasedCover), + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( + min=0, min_included=False + ), + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_START_SENSING_DELAY, default="500ms" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield cover.register_cover(var, config) + + yield automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) + + # OPEN + bin = yield 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] + ) + ) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) + yield automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) + + # CLOSE + bin = yield 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] + ) + ) + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + yield 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])) + 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] + ) + 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 new file mode 100644 index 0000000000..9f0a59377d --- /dev/null +++ b/esphome/components/current_based/current_based_cover.cpp @@ -0,0 +1,251 @@ +#include "current_based_cover.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace current_based { + +static const char *const TAG = "current_based.cover"; + +using namespace esphome::cover; + +CoverTraits CurrentBasedCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_position(true); + traits.set_is_assumed_state(false); + return traits; +} +void CurrentBasedCover::control(const CoverCall &call) { + if (call.get_stop()) { + this->direction_idle_(); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + if (pos == this->position) { + // already at target + } else { + auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; + this->target_position_ = pos; + this->start_direction_(op); + } + } +} +void CurrentBasedCover::setup() { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + this->position = 0.5f; + } +} + +void CurrentBasedCover::loop() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + + if (this->current_operation == COVER_OPERATION_OPENING) { + if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction + this->direction_idle_(); + this->malfunction_trigger_->trigger(); + ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit", + this->name_.c_str()); + } else if (this->is_opening_blocked_()) { // Blocked + ESP_LOGD(TAG, "'%s' - Obstacle detected during opening.", this->name_.c_str()); + this->direction_idle_(); + if (this->obstacle_rollback_ != 0) { + this->set_timeout("rollback", 300, [this]() { + ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str()); + this->target_position_ = clamp(this->position - this->obstacle_rollback_, 0.0F, 1.0F); + this->start_direction_(COVER_OPERATION_CLOSING); + }); + } + } else if (this->is_initial_delay_finished_() && !this->is_opening_()) { // End reached + auto dur = (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - Open position reached. Took %.1fs.", this->name_.c_str(), dur); + this->direction_idle_(COVER_OPEN); + } + } else if (this->current_operation == COVER_OPERATION_CLOSING) { + if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction + this->direction_idle_(); + this->malfunction_trigger_->trigger(); + ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit", + this->name_.c_str()); + } else if (this->is_closing_blocked_()) { // Blocked + ESP_LOGD(TAG, "'%s' - Obstacle detected during closing.", this->name_.c_str()); + this->direction_idle_(); + if (this->obstacle_rollback_ != 0) { + this->set_timeout("rollback", 300, [this]() { + ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str()); + this->target_position_ = clamp(this->position + this->obstacle_rollback_, 0.0F, 1.0F); + this->start_direction_(COVER_OPERATION_OPENING); + }); + } + } else if (this->is_initial_delay_finished_() && !this->is_closing_()) { // End reached + auto dur = (now - this->start_dir_time_) / 1e3f; + 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_) { + ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); + this->direction_idle_(); + } + + // Recompute position every loop cycle + this->recompute_position_(); + + if (this->current_operation != COVER_OPERATION_IDLE && this->is_at_target_()) { + this->direction_idle_(); + } + + // Send current position every second + if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > 1000) { + this->publish_state(false); + this->last_publish_time_ = now; + } +} + +void CurrentBasedCover::direction_idle_(float new_position) { + this->start_direction_(COVER_OPERATION_IDLE); + if (new_position != FLT_MAX) { + this->position = new_position; + } + this->publish_state(); +} + +void CurrentBasedCover::dump_config() { + LOG_COVER("", "Endstop Cover", this); + LOG_SENSOR(" ", "Open Sensor", this->open_sensor_); + ESP_LOGCONFIG(TAG, " Open moving current threshold: %.11fA", this->open_moving_current_threshold_); + if (this->open_obstacle_current_threshold_ != FLT_MAX) { + ESP_LOGCONFIG(TAG, " Open obstacle current threshold: %.11fA", this->open_obstacle_current_threshold_); + } + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); + LOG_SENSOR(" ", "Close Sensor", this->close_sensor_); + ESP_LOGCONFIG(TAG, " Close moving current threshold: %.11fA", this->close_moving_current_threshold_); + if (this->close_obstacle_current_threshold_ != FLT_MAX) { + ESP_LOGCONFIG(TAG, " Close obstacle current threshold: %.11fA", this->close_obstacle_current_threshold_); + } + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100); + if (this->max_duration_ != UINT32_MAX) { + ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f); + } + ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f); + ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_)); +} + +float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; } +void CurrentBasedCover::stop_prev_trigger_() { + if (this->prev_command_trigger_ != nullptr) { + this->prev_command_trigger_->stop_action(); + this->prev_command_trigger_ = nullptr; + } +} + +bool CurrentBasedCover::is_opening_() const { + return this->open_sensor_->get_state() > this->open_moving_current_threshold_; +} + +bool CurrentBasedCover::is_opening_blocked_() const { + if (this->open_obstacle_current_threshold_ == FLT_MAX) { + return false; + } + return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_; +} + +bool CurrentBasedCover::is_closing_() const { + return this->close_sensor_->get_state() > this->close_moving_current_threshold_; +} + +bool CurrentBasedCover::is_closing_blocked_() const { + if (this->close_obstacle_current_threshold_ == FLT_MAX) { + return false; + } + return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_; +} +bool CurrentBasedCover::is_initial_delay_finished_() const { + return millis() - this->start_dir_time_ > this->start_sensing_delay_; +} + +bool CurrentBasedCover::is_at_target_() const { + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + if (this->target_position_ == COVER_OPEN) { + if (!this->is_initial_delay_finished_()) // During initial delay, state is assumed + return false; + return !this->is_opening_(); + } + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + if (this->target_position_ == COVER_CLOSED) { + if (!this->is_initial_delay_finished_()) // During initial delay, state is assumed + return false; + return !this->is_closing_(); + } + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + default: + return true; + } +} +void CurrentBasedCover::start_direction_(CoverOperation dir) { + if (dir == this->current_operation) + return; + + this->recompute_position_(); + Trigger<> *trig; + switch (dir) { + case COVER_OPERATION_IDLE: + trig = this->stop_trigger_; + break; + case COVER_OPERATION_OPENING: + trig = this->open_trigger_; + break; + case COVER_OPERATION_CLOSING: + trig = this->close_trigger_; + break; + default: + return; + } + + this->current_operation = dir; + + this->stop_prev_trigger_(); + trig->trigger(); + this->prev_command_trigger_ = trig; + + const auto now = millis(); + this->start_dir_time_ = now; + this->last_recompute_time_ = now; +} +void CurrentBasedCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + float dir; + float action_dur; + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + dir = 1.0F; + action_dur = this->open_duration_; + break; + case COVER_OPERATION_CLOSING: + dir = -1.0F; + action_dur = this->close_duration_; + break; + default: + return; + } + + const auto now = millis(); + this->position += dir * (now - this->last_recompute_time_) / action_dur; + this->position = clamp(this->position, 0.0F, 1.0F); + + this->last_recompute_time_ = now; +} + +} // namespace current_based +} // namespace esphome diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h new file mode 100644 index 0000000000..220b770c05 --- /dev/null +++ b/esphome/components/current_based/current_based_cover.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/components/cover/cover.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace current_based { + +class CurrentBasedCover : public cover::Cover, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + + Trigger<> *get_open_trigger() const { return this->open_trigger_; } + void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; } + void set_open_moving_current_threshold(float open_moving_current_threshold) { + this->open_moving_current_threshold_ = open_moving_current_threshold; + } + void set_open_obstacle_current_threshold(float open_obstacle_current_threshold) { + this->open_obstacle_current_threshold_ = open_obstacle_current_threshold; + } + void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } + + Trigger<> *get_close_trigger() const { return this->close_trigger_; } + void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; } + void set_close_moving_current_threshold(float close_moving_current_threshold) { + this->close_moving_current_threshold_ = close_moving_current_threshold; + } + void set_close_obstacle_current_threshold(float close_obstacle_current_threshold) { + this->close_obstacle_current_threshold_ = close_obstacle_current_threshold; + } + void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } + + void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; } + void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; } + + void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; } + void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; } + + Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; } + + cover::CoverTraits get_traits() override; + + protected: + void control(const cover::CoverCall &call) override; + void stop_prev_trigger_(); + + bool is_at_target_() const; + bool is_opening_() const; + bool is_opening_blocked_() const; + bool is_closing_() const; + bool is_closing_blocked_() const; + bool is_initial_delay_finished_() const; + + void direction_idle_(float new_position = FLT_MAX); + void start_direction_(cover::CoverOperation dir); + + void recompute_position_(); + + Trigger<> *stop_trigger_{new Trigger<>()}; + + sensor::Sensor *open_sensor_{nullptr}; + Trigger<> *open_trigger_{new Trigger<>()}; + float open_moving_current_threshold_; + float open_obstacle_current_threshold_{FLT_MAX}; + uint32_t open_duration_; + + sensor::Sensor *close_sensor_{nullptr}; + Trigger<> *close_trigger_{new Trigger<>()}; + float close_moving_current_threshold_; + float close_obstacle_current_threshold_{FLT_MAX}; + uint32_t close_duration_; + + uint32_t max_duration_{UINT32_MAX}; + bool malfunction_detection_{true}; + Trigger<> *malfunction_trigger_{new Trigger<>()}; + uint32_t start_sensing_delay_; + float obstacle_rollback_; + + Trigger<> *prev_command_trigger_{nullptr}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + uint32_t last_publish_time_{0}; + float target_position_{0}; +}; + +} // namespace current_based +} // namespace esphome diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 3351a98d24..2b7698ce5a 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -16,10 +16,7 @@ class CWWWLightOutput : public light::LightOutput { void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -34,8 +31,8 @@ class CWWWLightOutput : public light::LightOutput { protected: output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - float cold_white_temperature_; - float warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; }; diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 674c48d219..fc204b2f3b 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( + CONF_CONSTANT_BRIGHTNESS, CONF_OUTPUT_ID, CONF_COLD_WHITE, CONF_WARM_WHITE, @@ -12,28 +13,40 @@ from esphome.const import ( cwww_ns = cg.esphome_ns.namespace("cwww") CWWWLightOutput = cwww_ns.class_("CWWWLightOutput", light.LightOutput) -CONF_CONSTANT_BRIGHTNESS = "constant_brightness" - -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), - cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, - } +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), + cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + } + ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), + light.validate_color_temperature_channels, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) + cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index b426b85183..5f8d0288e2 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -77,7 +77,7 @@ uint8_t DaikinClimate::operation_mode_() { case climate::CLIMATE_MODE_HEAT: operating_mode |= DAIKIN_MODE_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: operating_mode |= DAIKIN_MODE_AUTO; break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -131,11 +131,11 @@ uint8_t DaikinClimate::temperature_() { switch (this->mode) { case climate::CLIMATE_MODE_FAN_ONLY: return 0x32; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_DRY: return 0xc0; default: - uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); + uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); return temperature << 1; } } @@ -160,7 +160,7 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { this->mode = climate::CLIMATE_MODE_HEAT; break; case DAIKIN_MODE_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; case DAIKIN_MODE_FAN: this->mode = climate::CLIMATE_MODE_FAN_ONLY; diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index c0a472bce7..b4ac309de9 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -43,12 +43,11 @@ const uint8_t DAIKIN_STATE_FRAME_SIZE = 19; class DaikinClimate : public climate_ir::ClimateIR { public: DaikinClimate() - : climate_ir::ClimateIR( - DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, - std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, - std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_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}) {} protected: // Transmit via IR the state of this climate controller. diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 762bfdc3c3..2dbc69b8e2 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -11,13 +11,17 @@ dallas_ns = cg.esphome_ns.namespace("dallas") DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) ESPOneWire = dallas_ns.class_("ESPOneWire") -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(DallasComponent), - cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - } -).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DallasComponent), + cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + } + ).extend(cv.polling_component_schema("60s")), + # pin_mode call logs in esp-idf, but InterruptLock is active -> crash + cv.only_with_arduino, +) async def to_code(config): diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 7fc5e424f0..0fc4108687 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -97,16 +97,7 @@ void DallasComponent::dump_config() { } } -DallasTemperatureSensor *DallasComponent::get_sensor_by_address(uint64_t address, uint8_t resolution) { - auto s = new DallasTemperatureSensor(address, resolution, this); - this->sensors_.push_back(s); - return s; -} -DallasTemperatureSensor *DallasComponent::get_sensor_by_index(uint8_t index, uint8_t resolution) { - auto s = this->get_sensor_by_address(0, resolution); - s->set_index(index); - return s; -} +void DallasComponent::register_sensor(DallasTemperatureSensor *sensor) { this->sensors_.push_back(sensor); } void DallasComponent::update() { this->status_clear_warning(); @@ -137,7 +128,7 @@ void DallasComponent::update() { } if (!res) { - ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); sensor->publish_state(NAN); this->status_set_warning(); return; @@ -157,11 +148,6 @@ void DallasComponent::update() { } DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} -DallasTemperatureSensor::DallasTemperatureSensor(uint64_t address, uint8_t resolution, DallasComponent *parent) - : parent_(parent) { - this->set_address(address); - this->set_resolution(resolution); -} 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; } @@ -175,7 +161,7 @@ const std::string &DallasTemperatureSensor::get_address_name() { return this->address_name_; } -bool ICACHE_RAM_ATTR DallasTemperatureSensor::read_scratch_pad() { +bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { ESPOneWire *wire = this->parent_->one_wire_; if (!wire->reset()) { return false; diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index d32aec1758..8d405f6eab 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -13,8 +13,7 @@ class DallasComponent : public PollingComponent { public: explicit DallasComponent(ESPOneWire *one_wire); - DallasTemperatureSensor *get_sensor_by_address(uint64_t address, uint8_t resolution); - DallasTemperatureSensor *get_sensor_by_index(uint8_t index, uint8_t resolution); + void register_sensor(DallasTemperatureSensor *sensor); void setup() override; void dump_config() override; @@ -33,8 +32,7 @@ class DallasComponent : public PollingComponent { /// Internal class that helps us create multiple sensors for one Dallas hub. class DallasTemperatureSensor : public sensor::Sensor { public: - DallasTemperatureSensor(uint64_t address, uint8_t resolution, DallasComponent *parent); - + 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". diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 702d1eddc2..9278b83f7f 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -12,11 +12,11 @@ const int ONE_WIRE_ROM_SEARCH = 0xF0; ESPOneWire::ESPOneWire(GPIOPin *pin) : pin_(pin) {} -bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { +bool HOT IRAM_ATTR ESPOneWire::reset() { uint8_t retries = 125; // Wait for communication to clear - this->pin_->pin_mode(INPUT_PULLUP); + this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); do { if (--retries == 0) return false; @@ -24,12 +24,12 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { } while (!this->pin_->digital_read()); // Send 480µs LOW TX reset pulse - this->pin_->pin_mode(OUTPUT); + this->pin_->pin_mode(gpio::FLAG_OUTPUT); this->pin_->digital_write(false); delayMicroseconds(480); // Switch into RX mode, letting the pin float - this->pin_->pin_mode(INPUT_PULLUP); + this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); // after 15µs-60µs wait time, responder pulls low for 60µs-240µs // let's have 70µs just in case delayMicroseconds(70); @@ -39,9 +39,9 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { return r; } -void HOT ICACHE_RAM_ATTR ESPOneWire::write_bit(bool bit) { +void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { // Initiate write/read by pulling low. - this->pin_->pin_mode(OUTPUT); + this->pin_->pin_mode(gpio::FLAG_OUTPUT); this->pin_->digital_write(false); // bus sampled within 15µs and 60µs after pulling LOW. @@ -60,14 +60,14 @@ void HOT ICACHE_RAM_ATTR ESPOneWire::write_bit(bool bit) { } } -bool HOT ICACHE_RAM_ATTR ESPOneWire::read_bit() { +bool HOT IRAM_ATTR ESPOneWire::read_bit() { // Initiate read slot by pulling LOW for at least 1µs - this->pin_->pin_mode(OUTPUT); + this->pin_->pin_mode(gpio::FLAG_OUTPUT); this->pin_->digital_write(false); delayMicroseconds(3); // release bus, we have to sample within 15µs of pulling low - this->pin_->pin_mode(INPUT_PULLUP); + this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); delayMicroseconds(10); bool r = this->pin_->digital_read(); @@ -76,43 +76,43 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::read_bit() { return r; } -void ICACHE_RAM_ATTR ESPOneWire::write8(uint8_t val) { +void IRAM_ATTR ESPOneWire::write8(uint8_t val) { for (uint8_t i = 0; i < 8; i++) { this->write_bit(bool((1u << i) & val)); } } -void ICACHE_RAM_ATTR ESPOneWire::write64(uint64_t 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 ICACHE_RAM_ATTR ESPOneWire::read8() { +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 ICACHE_RAM_ATTR ESPOneWire::read64() { +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 ICACHE_RAM_ATTR ESPOneWire::select(uint64_t address) { +void IRAM_ATTR ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void ICACHE_RAM_ATTR ESPOneWire::reset_search() { +void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } -uint64_t HOT ICACHE_RAM_ATTR ESPOneWire::search() { +uint64_t HOT IRAM_ATTR ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } @@ -196,7 +196,7 @@ uint64_t HOT ICACHE_RAM_ATTR ESPOneWire::search() { return this->rom_number_; } -std::vector ICACHE_RAM_ATTR ESPOneWire::search_vec() { +std::vector IRAM_ATTR ESPOneWire::search_vec() { std::vector res; this->reset_search(); @@ -206,12 +206,12 @@ std::vector ICACHE_RAM_ATTR ESPOneWire::search_vec() { return res; } -void ICACHE_RAM_ATTR ESPOneWire::skip() { +void IRAM_ATTR ESPOneWire::skip() { this->write8(0xCC); // skip ROM } GPIOPin *ESPOneWire::get_pin() { return this->pin_; } -uint8_t ICACHE_RAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } +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 index 68bcc0c193..728fa127d3 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -1,7 +1,8 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" +#include namespace esphome { namespace dallas { diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 1c8db8fa2f..14ad0efa7b 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_INDEX, CONF_RESOLUTION, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, CONF_ID, @@ -18,7 +17,10 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), @@ -34,10 +36,17 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): hub = await cg.get_variable(config[CONF_DALLAS_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + if CONF_ADDRESS in config: - address = config[CONF_ADDRESS] - rhs = hub.Pget_sensor_by_address(address, config.get(CONF_RESOLUTION)) + cg.add(var.set_address(config[CONF_ADDRESS])) else: - rhs = hub.Pget_sensor_by_index(config[CONF_INDEX], config.get(CONF_RESOLUTION)) - var = cg.Pvariable(config[CONF_ID], rhs) + 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)) await sensor.register_sensor(var, config) diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py new file mode 100644 index 0000000000..45b8f98f0c --- /dev/null +++ b/esphome/components/daly_bms/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@s1lvi0"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor", "text_sensor", "binary_sensor"] + +CONF_BMS_DALY_ID = "bms_daly_id" + +daly_bms = cg.esphome_ns.namespace("daly_bms") +DalyBmsComponent = daly_bms.class_( + "DalyBmsComponent", cg.PollingComponent, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)}) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("30s")) +) + + +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/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py new file mode 100644 index 0000000000..23330cd945 --- /dev/null +++ b/esphome/components/daly_bms/binary_sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID +from . import DalyBmsComponent, CONF_BMS_DALY_ID + +CONF_CHARGING_MOS_ENABLED = "charging_mos_enabled" +CONF_DISCHARGING_MOS_ENABLED = "discharging_mos_enabled" + +TYPES = [ + CONF_CHARGING_MOS_ENABLED, + CONF_DISCHARGING_MOS_ENABLED, +] + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), + cv.Optional( + CONF_CHARGING_MOS_ENABLED + ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), + } + ), + cv.Optional( + CONF_DISCHARGING_MOS_ENABLED + ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + + +async def setup_conf(config, key, hub): + if key in config: + conf = config[key] + sens = cg.new_Pvariable(conf[CONF_ID]) + await binary_sensor.register_binary_sensor(sens, conf) + cg.add(getattr(hub, f"set_{key}_binary_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp new file mode 100644 index 0000000000..44c05f0686 --- /dev/null +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -0,0 +1,181 @@ +#include "daly_bms.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace daly_bms { + +static const char *const TAG = "daly_bms"; + +static const uint8_t DALY_FRAME_SIZE = 13; +static const uint8_t DALY_TEMPERATURE_OFFSET = 40; +static const uint16_t DALY_CURRENT_OFFSET = 30000; + +static const uint8_t DALY_REQUEST_BATTERY_LEVEL = 0x90; +static const uint8_t DALY_REQUEST_MIN_MAX_VOLTAGE = 0x91; +static const uint8_t DALY_REQUEST_MIN_MAX_TEMPERATURE = 0x92; +static const uint8_t DALY_REQUEST_MOS = 0x93; +static const uint8_t DALY_REQUEST_STATUS = 0x94; +static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96; + +void DalyBmsComponent::setup() {} + +void DalyBmsComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Daly BMS:"); + this->check_uart_settings(9600); +} + +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_TEMPERATURE); + + 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); + } +} + +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] = 0x80; // 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) + + this->write_array(request_message, sizeof(request_message)); + this->flush(); +} + +void DalyBmsComponent::decode_data_(std::vector data) { + auto it = data.begin(); + + while ((it = std::find(it, data.end(), 0xA5)) != data.end()) { + if (data.end() - it >= DALY_FRAME_SIZE && it[1] == 0x01) { + uint8_t checksum; + int sum = 0; + for (int i = 0; i < 12; i++) { + sum += it[i]; + } + checksum = sum; + + if (checksum == it[12]) { + switch (it[2]) { + case DALY_REQUEST_BATTERY_LEVEL: + if (this->voltage_sensor_) { + this->voltage_sensor_->publish_state((float) encode_uint16(it[4], it[5]) / 10); + } + if (this->current_sensor_) { + this->current_sensor_->publish_state(((float) (encode_uint16(it[8], it[9]) - DALY_CURRENT_OFFSET) / 10)); + } + if (this->battery_level_sensor_) { + this->battery_level_sensor_->publish_state((float) encode_uint16(it[10], it[11]) / 10); + } + 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_number_) { + this->max_cell_voltage_number_->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_number_) { + this->min_cell_voltage_number_->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_probe_number_) { + this->max_temperature_probe_number_->publish_state(it[5]); + } + if (this->min_temperature_) { + this->min_temperature_->publish_state(it[6] - DALY_TEMPERATURE_OFFSET); + } + if (this->min_temperature_probe_number_) { + this->min_temperature_probe_number_->publish_state(it[7]); + } + break; + + case DALY_REQUEST_MOS: + if (this->status_text_sensor_ != nullptr) { + switch (it[4]) { + case 0: + this->status_text_sensor_->publish_state("Stationary"); + break; + case 1: + this->status_text_sensor_->publish_state("Charging"); + break; + case 2: + this->status_text_sensor_->publish_state("Discharging"); + break; + default: + break; + } + } + if (this->charging_mos_enabled_) { + this->charging_mos_enabled_->publish_state(it[5]); + } + if (this->discharging_mos_enabled_) { + this->discharging_mos_enabled_->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); + } + break; + + case DALY_REQUEST_STATUS: + if (this->cells_number_) { + this->cells_number_->publish_state(it[4]); + } + break; + + case DALY_REQUEST_TEMPERATURE: + if (it[4] == 1) { + if (this->temperature_1_sensor_) { + this->temperature_1_sensor_->publish_state(it[5] - DALY_TEMPERATURE_OFFSET); + } + if (this->temperature_2_sensor_) { + this->temperature_2_sensor_->publish_state(it[6] - DALY_TEMPERATURE_OFFSET); + } + } + break; + + default: + break; + } + } + std::advance(it, DALY_FRAME_SIZE); + } else { + std::advance(it, 1); + } + } +} + +} // namespace daly_bms +} // namespace esphome diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h new file mode 100644 index 0000000000..b5d4c8ae39 --- /dev/null +++ b/esphome/components/daly_bms/daly_bms.h @@ -0,0 +1,83 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace daly_bms { + +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; } + // 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; + } + + void setup() override; + void dump_config() override; + void update() override; + + float get_setup_priority() const override; + + protected: + void request_data_(uint8_t data_id); + void decode_data_(std::vector data); + + 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}; + + text_sensor::TextSensor *status_text_sensor_{nullptr}; + + binary_sensor::BinarySensor *charging_mos_enabled_{nullptr}; + binary_sensor::BinarySensor *discharging_mos_enabled_{nullptr}; +}; + +} // namespace daly_bms +} // namespace esphome diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py new file mode 100644 index 0000000000..1d0ee89914 --- /dev/null +++ b/esphome/components/daly_bms/sensor.py @@ -0,0 +1,192 @@ +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_BATTERY_LEVEL, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_PERCENT, + UNIT_CELSIUS, + UNIT_EMPTY, + ICON_FLASH, + ICON_PERCENT, + ICON_COUNTER, + ICON_THERMOMETER, + ICON_GAUGE, +) +from . import DalyBmsComponent, CONF_BMS_DALY_ID + +CONF_MAX_CELL_VOLTAGE = "max_cell_voltage" +CONF_MAX_CELL_VOLTAGE_NUMBER = "max_cell_voltage_number" +CONF_MIN_CELL_VOLTAGE = "min_cell_voltage" +CONF_MIN_CELL_VOLTAGE_NUMBER = "min_cell_voltage_number" +CONF_MAX_TEMPERATURE_PROBE_NUMBER = "max_temperature_probe_number" +CONF_MIN_TEMPERATURE_PROBE_NUMBER = "min_temperature_probe_number" +CONF_CELLS_NUMBER = "cells_number" + +CONF_REMAINING_CAPACITY = "remaining_capacity" +CONF_TEMPERATURE_1 = "temperature_1" +CONF_TEMPERATURE_2 = "temperature_2" + +ICON_CURRENT_DC = "mdi:current-dc" +ICON_BATTERY_OUTLINE = "mdi:battery-outline" +ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" +ICON_THERMOMETER_CHEVRON_DOWN = "mdi:thermometer-chevron-down" +ICON_CAR_BATTERY = "mdi:car-battery" + +UNIT_AMPERE_HOUR = "Ah" + +TYPES = [ + CONF_VOLTAGE, + CONF_CURRENT, + CONF_BATTERY_LEVEL, + CONF_MAX_CELL_VOLTAGE, + CONF_MAX_CELL_VOLTAGE_NUMBER, + CONF_MIN_CELL_VOLTAGE, + CONF_MIN_CELL_VOLTAGE_NUMBER, + CONF_MAX_TEMPERATURE, + CONF_MAX_TEMPERATURE_PROBE_NUMBER, + CONF_MIN_TEMPERATURE, + CONF_MIN_TEMPERATURE_PROBE_NUMBER, + CONF_CELLS_NUMBER, + CONF_REMAINING_CAPACITY, + CONF_TEMPERATURE_1, + CONF_TEMPERATURE_2, +] + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, + ICON_FLASH, + 1, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + UNIT_AMPERE, + ICON_CURRENT_DC, + 1, + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, + ICON_PERCENT, + 1, + DEVICE_CLASS_BATTERY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MAX_CELL_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, + ICON_FLASH, + 2, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MAX_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( + UNIT_EMPTY, + ICON_COUNTER, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_MIN_CELL_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, + ICON_FLASH, + 2, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MIN_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( + UNIT_EMPTY, + ICON_COUNTER, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_MAX_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_THERMOMETER_CHEVRON_UP, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MAX_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( + UNIT_EMPTY, + ICON_COUNTER, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_MIN_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_THERMOMETER_CHEVRON_DOWN, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MIN_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( + UNIT_EMPTY, + ICON_COUNTER, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema( + UNIT_AMPERE_HOUR, + ICON_GAUGE, + 2, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CELLS_NUMBER): sensor.sensor_schema( + UNIT_EMPTY, + ICON_COUNTER, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_THERMOMETER, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_THERMOMETER, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + + +async def setup_conf(config, key, hub): + if key in config: + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py new file mode 100644 index 0000000000..de49a0b4b9 --- /dev/null +++ b/esphome/components/daly_bms/text_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ICON, CONF_ID, CONF_STATUS +from . import DalyBmsComponent, CONF_BMS_DALY_ID + +ICON_CAR_BATTERY = "mdi:car-battery" + +TYPES = [ + CONF_STATUS, +] + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), + cv.Optional(CONF_STATUS): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_CAR_BATTERY): cv.icon, + } + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + + +async def setup_conf(config, key, hub): + if key in config: + conf = config[key] + sens = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(sens, conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BMS_DALY_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py new file mode 100644 index 0000000000..2b884d3b9a --- /dev/null +++ b/esphome/components/dashboard_import/__init__.py @@ -0,0 +1,45 @@ +from pathlib import Path + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.packages import validate_source_shorthand +from esphome.yaml_util import dump + + +dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") + +# payload is in `esphomelib` mdns record, which only exists if api +# is enabled +DEPENDENCIES = ["api"] +CODEOWNERS = ["@esphome/core"] + + +def validate_import_url(value): + value = cv.string_strict(value) + value = cv.Length(max=255)(value) + # ignore result, only check if it's a valid shorthand + validate_source_shorthand(value) + return value + + +CONF_PACKAGE_IMPORT_URL = "package_import_url" +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url, + } +) + + +async def to_code(config): + cg.add_define("USE_DASHBOARD_IMPORT") + cg.add(dashboard_import_ns.set_package_import_url(config[CONF_PACKAGE_IMPORT_URL])) + + +def import_config(path: str, name: str, project_name: str, import_url: str) -> None: + p = Path(path) + + if p.exists(): + raise FileExistsError + + config = {"substitutions": {"name": name}, "packages": {project_name: import_url}} + p.write_text(dump(config), encoding="utf8") diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp new file mode 100644 index 0000000000..6875fd61a5 --- /dev/null +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -0,0 +1,12 @@ +#include "dashboard_import.h" + +namespace esphome { +namespace dashboard_import { + +static std::string g_package_import_url; // NOLINT + +std::string get_package_import_url() { return g_package_import_url; } +void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } + +} // namespace dashboard_import +} // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h new file mode 100644 index 0000000000..0ca2994aab --- /dev/null +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace esphome { +namespace dashboard_import { + +std::string get_package_import_url(); +void set_package_import_url(std::string url); + +} // namespace dashboard_import +} // namespace esphome diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 668792c7b1..b856733121 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -4,8 +4,18 @@ #include "esphome/core/defines.h" #include "esphome/core/version.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include +#include +#endif + +#ifdef USE_ARDUINO +#include +#endif + +#ifdef USE_ESP_IDF +#include +#include #endif namespace esphome { @@ -21,11 +31,16 @@ void DebugComponent::dump_config() { #endif ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); - this->free_heap_ = ESP.getFreeHeap(); +#ifdef USE_ARDUINO + this->free_heap_ = ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP_IDF) + this->free_heap_ = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); +#endif ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); +#ifdef USE_ARDUINO const char *flash_mode; - switch (ESP.getFlashChipMode()) { + switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) case FM_QIO: flash_mode = "QIO"; break; @@ -38,7 +53,7 @@ void DebugComponent::dump_config() { case FM_DOUT: flash_mode = "DOUT"; break; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 case FM_FAST_READ: flash_mode = "FAST_READ"; break; @@ -49,10 +64,12 @@ void DebugComponent::dump_config() { default: flash_mode = "UNKNOWN"; } + // NOLINTNEXTLINE(readability-static-accessed-through-instance) ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s", ESP.getFlashChipSize() / 1024, ESP.getFlashChipSpeed() / 1000000, flash_mode); +#endif // USE_ARDUINO -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 esp_chip_info_t info; esp_chip_info(&info); const char *model; @@ -87,8 +104,7 @@ void DebugComponent::dump_config() { ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - std::string mac = uint64_to_string(ESP.getEfuseMac()); - ESP_LOGD(TAG, "EFuse MAC: %s", mac.c_str()); + ESP_LOGD(TAG, "EFuse MAC: %s", get_mac_address_pretty().c_str()); const char *reset_reason; switch (rtc_get_reset_reason(0)) { @@ -186,7 +202,7 @@ void DebugComponent::dump_config() { ESP_LOGD(TAG, "Wakeup Reason: %s", wakeup_reason); #endif -#ifdef ARDUINO_ARCH_ESP8266 +#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()); @@ -198,7 +214,11 @@ void DebugComponent::dump_config() { #endif } void DebugComponent::loop() { - uint32_t new_free_heap = ESP.getFreeHeap(); +#ifdef USE_ARDUINO + uint32_t new_free_heap = ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP_IDF) + uint32_t new_free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); +#endif 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_); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 7011081774..f47888b8eb 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_MODE, CONF_NUMBER, CONF_PINS, - CONF_RUN_CYCLES, CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN, @@ -17,8 +16,7 @@ def validate_pin_number(value): valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( - "Only pins {} support wakeup" - "".format(", ".join(str(x) for x in valid_pins)) + f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup" ) return value @@ -46,6 +44,7 @@ EXT1_WAKEUP_MODES = { CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode" CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup" +CONF_TOUCH_WAKEUP = "touch_wakeup" CONFIG_SCHEMA = cv.Schema( { @@ -63,17 +62,13 @@ CONFIG_SCHEMA = cv.Schema( cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( - pins.shorthand_input_pin, validate_pin_number + pins.internal_gpio_input_pin_schema, validate_pin_number ), cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), } ), ), - cv.Optional(CONF_RUN_CYCLES): cv.invalid( - "The run_cycles option has been removed in 1.11.0 as " - "it was essentially the same as a run_duration of 0s." - "Please use run_duration now." - ), + cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), } ).extend(cv.COMPONENT_SCHEMA) @@ -102,6 +97,9 @@ async def to_code(config): ) cg.add(var.set_ext1_wakeup(struct)) + if CONF_TOUCH_WAKEUP in config: + cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) + cg.add_define("USE_DEEP_SLEEP") diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index de5672759a..e4b1edfb7b 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -2,6 +2,10 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#ifdef USE_ESP8266 +#include +#endif + namespace esphome { namespace deep_sleep { @@ -25,9 +29,9 @@ void DeepSleepComponent::dump_config() { if (this->run_duration_.has_value()) { ESP_LOGCONFIG(TAG, " Run Duration: %u ms", *this->run_duration_); } -#ifdef ARDUINO_ARCH_ESP32 - if (this->wakeup_pin_.has_value()) { - LOG_PIN(" Wakeup Pin: ", *this->wakeup_pin_); +#ifdef USE_ESP32 + if (wakeup_pin_ != nullptr) { + LOG_PIN(" Wakeup Pin: ", this->wakeup_pin_); } #endif } @@ -39,11 +43,12 @@ 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; } -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } 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(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { @@ -51,9 +56,9 @@ void DeepSleepComponent::begin_sleep(bool manual) { this->next_enter_deep_sleep_ = true; return; } -#ifdef ARDUINO_ARCH_ESP32 - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_KEEP_AWAKE && this->wakeup_pin_.has_value() && - !this->sleep_duration_.has_value() && (*this->wakeup_pin_)->digital_read()) { +#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(); @@ -68,23 +73,29 @@ void DeepSleepComponent::begin_sleep(bool manual) { App.run_safe_shutdown_hooks(); -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - if (this->wakeup_pin_.has_value()) { - bool level = !(*this->wakeup_pin_)->is_inverted(); - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && (*this->wakeup_pin_)->digital_read()) + 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); + 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); + } + esp_deep_sleep_start(); #endif -#ifdef ARDUINO_ARCH_ESP8266 - ESP.deepSleep(*this->sleep_duration_); +#ifdef USE_ESP8266 + ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) #endif } float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index e163cf7709..d7969ba999 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -3,11 +3,16 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/automation.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP32 +#include +#endif namespace esphome { namespace deep_sleep { -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 /** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32 * and the scenario occurs that the wakeup pin is already in the wakeup state. @@ -43,15 +48,17 @@ class DeepSleepComponent : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. void set_sleep_duration(uint32_t time_ms); -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. */ - void set_wakeup_pin(GPIOPin *pin) { this->wakeup_pin_ = pin; } + void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); + + void set_touch_wakeup(bool touch_wakeup); #endif /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); @@ -69,10 +76,11 @@ class DeepSleepComponent : public Component { protected: optional sleep_duration_; -#ifdef ARDUINO_ARCH_ESP32 - optional wakeup_pin_; +#ifdef USE_ESP32 + InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; optional ext1_wakeup_; + optional touch_wakeup_; #endif optional run_duration_; bool next_enter_deep_sleep_{false}; diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py new file mode 100644 index 0000000000..fae8a2b07d --- /dev/null +++ b/esphome/components/demo/__init__.py @@ -0,0 +1,449 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + climate, + cover, + fan, + light, + number, + sensor, + switch, + text_sensor, +) +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_BINARY_SENSORS, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_ICON, + CONF_ID, + CONF_INVERTED, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_NAME, + CONF_OUTPUT_ID, + CONF_SENSORS, + CONF_STATE_CLASS, + CONF_STEP, + CONF_SWITCHES, + CONF_TEXT_SENSORS, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_TEMPERATURE, + ICON_BLUETOOTH, + ICON_BLUR, + ICON_EMPTY, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_PERCENT, + UNIT_WATT_HOURS, +) + +AUTO_LOAD = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "number", + "sensor", + "switch", + "text_sensor", +] + +demo_ns = cg.esphome_ns.namespace("demo") +DemoBinarySensor = demo_ns.class_( + "DemoBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) +DemoClimate = demo_ns.class_("DemoClimate", climate.Climate, cg.Component) +DemoClimateType = demo_ns.enum("DemoClimateType", is_class=True) +DemoCover = demo_ns.class_("DemoCover", cover.Cover, cg.Component) +DemoCoverType = demo_ns.enum("DemoCoverType", is_class=True) +DemoFan = demo_ns.class_("DemoFan", cg.Component) +DemoFanType = demo_ns.enum("DemoFanType", is_class=True) +DemoLight = demo_ns.class_("DemoLight", light.LightOutput, cg.Component) +DemoLightType = demo_ns.enum("DemoLightType", is_class=True) +DemoNumber = demo_ns.class_("DemoNumber", number.Number, cg.Component) +DemoNumberType = demo_ns.enum("DemoNumberType", is_class=True) +DemoSensor = demo_ns.class_("DemoSensor", sensor.Sensor, cg.PollingComponent) +DemoSwitch = demo_ns.class_("DemoSwitch", switch.Switch, cg.Component) +DemoTextSensor = demo_ns.class_( + "DemoTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + + +CLIMATE_TYPES = { + 1: DemoClimateType.TYPE_1, + 2: DemoClimateType.TYPE_2, + 3: DemoClimateType.TYPE_3, +} +COVER_TYPES = { + 1: DemoCoverType.TYPE_1, + 2: DemoCoverType.TYPE_2, + 3: DemoCoverType.TYPE_3, + 4: DemoCoverType.TYPE_4, +} +FAN_TYPES = { + 1: DemoFanType.TYPE_1, + 2: DemoFanType.TYPE_2, + 3: DemoFanType.TYPE_3, + 4: DemoFanType.TYPE_4, +} +LIGHT_TYPES = { + 1: DemoLightType.TYPE_1, + 2: DemoLightType.TYPE_2, + 3: DemoLightType.TYPE_3, + 4: DemoLightType.TYPE_4, + 5: DemoLightType.TYPE_5, + 6: DemoLightType.TYPE_6, + 7: DemoLightType.TYPE_7, +} +NUMBER_TYPES = { + 1: DemoNumberType.TYPE_1, + 2: DemoNumberType.TYPE_2, + 3: DemoNumberType.TYPE_3, +} + + +CONF_CLIMATES = "climates" +CONF_COVERS = "covers" +CONF_FANS = "fans" +CONF_LIGHTS = "lights" +CONF_NUMBERS = "numbers" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional( + CONF_BINARY_SENSORS, + default=[ + { + CONF_NAME: "Demo Basement Floor Wet", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + }, + { + CONF_NAME: "Demo Movement Backyard", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOTION, + }, + ], + ): [ + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoBinarySensor), + } + ) + ], + cv.Optional( + CONF_CLIMATES, + default=[ + { + CONF_NAME: "Demo Heatpump", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo HVAC", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Ecobee", + CONF_TYPE: 3, + }, + ], + ): [ + climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoClimate), + cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_COVERS, + default=[ + { + CONF_NAME: "Demo Kitchen Window", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Garage Door", + CONF_TYPE: 2, + CONF_DEVICE_CLASS: "garage", + }, + { + CONF_NAME: "Demo Living Room Window", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Hall Window", + CONF_TYPE: 4, + CONF_DEVICE_CLASS: "window", + }, + ], + ): [ + cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoCover), + cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_FANS, + default=[ + { + CONF_NAME: "Demo Living Room Fan", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Ceiling Fan", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Percentage Limited Fan", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Percentage Full Fan", + CONF_TYPE: 4, + }, + ], + ): [ + fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan), + cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_LIGHTS, + default=[ + { + CONF_NAME: "Demo Binary Light", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Brightness Light", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo RGB Light", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo RGBW Light", + CONF_TYPE: 4, + }, + { + CONF_NAME: "Demo RGBWW Light", + CONF_TYPE: 5, + }, + { + CONF_NAME: "Demo CWWW Light", + CONF_TYPE: 6, + }, + { + CONF_NAME: "Demo RGBW interlock Light", + CONF_TYPE: 7, + }, + ], + ): [ + light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), + cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_NUMBERS, + default=[ + { + CONF_NAME: "Demo Number 0-100", + CONF_TYPE: 1, + CONF_MIN_VALUE: 0, + CONF_MAX_VALUE: 100, + CONF_STEP: 1, + }, + { + CONF_NAME: "Demo Number -50-50", + CONF_TYPE: 2, + CONF_MIN_VALUE: -50, + CONF_MAX_VALUE: 50, + CONF_STEP: 5, + }, + { + CONF_NAME: "Demo Number 40-60", + CONF_TYPE: 3, + CONF_MIN_VALUE: 40, + CONF_MAX_VALUE: 60, + CONF_STEP: 0.2, + }, + ], + ): [ + number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoNumber), + cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True), + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.float_, + } + ) + ], + cv.Optional( + CONF_SENSORS, + default=[ + { + CONF_NAME: "Demo Plain Sensor", + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Force Update Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENT, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_FORCE_UPDATE: True, + }, + { + CONF_NAME: "Demo Energy Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + }, + ], + ): [ + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0) + .extend(cv.polling_component_schema("60s")) + .extend( + { + cv.GenerateID(): cv.declare_id(DemoSensor), + } + ) + ], + cv.Optional( + CONF_SWITCHES, + default=[ + { + CONF_NAME: "Demo Switch 1", + }, + { + CONF_NAME: "Demo Switch 2", + CONF_INVERTED: True, + CONF_ICON: ICON_BLUETOOTH, + }, + ], + ): [ + switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoSwitch), + } + ) + ], + cv.Optional( + CONF_TEXT_SENSORS, + default=[ + { + CONF_NAME: "Demo Text Sensor 1", + }, + { + CONF_NAME: "Demo Text Sensor 2", + CONF_ICON: ICON_BLUR, + }, + ], + ): [ + text_sensor.TEXT_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoTextSensor), + } + ) + ], + } +) + + +async def to_code(config): + for conf in config[CONF_BINARY_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await binary_sensor.register_binary_sensor(var, conf) + + for conf in config[CONF_CLIMATES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await climate.register_climate(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_COVERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await cover.register_cover(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_FANS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + fan_ = await fan.create_fan_state(conf) + cg.add(var.set_fan(fan_)) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_LIGHTS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + await light.register_light(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_NUMBERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await number.register_number( + var, + conf, + min_value=conf[CONF_MIN_VALUE], + max_value=conf[CONF_MAX_VALUE], + step=conf[CONF_STEP], + ) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await sensor.register_sensor(var, conf) + + for conf in config[CONF_SWITCHES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + + for conf in config[CONF_TEXT_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await text_sensor.register_text_sensor(var, conf) diff --git a/esphome/components/demo/demo_binary_sensor.h b/esphome/components/demo/demo_binary_sensor.h new file mode 100644 index 0000000000..4dfd038761 --- /dev/null +++ b/esphome/components/demo/demo_binary_sensor.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace demo { + +class DemoBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { + public: + void setup() override { this->publish_initial_state(false); } + void update() override { + bool new_state = last_state_ = !last_state_; + this->publish_state(new_state); + } + + protected: + bool last_state_ = false; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h new file mode 100644 index 0000000000..0cf48dd4ee --- /dev/null +++ b/esphome/components/demo/demo_climate.h @@ -0,0 +1,157 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace demo { + +enum class DemoClimateType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoClimate : public climate::Climate, public Component { + public: + void set_type(DemoClimateType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoClimateType::TYPE_1: + this->current_temperature = 20.0; + this->target_temperature = 21.0; + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + break; + case DemoClimateType::TYPE_2: + this->target_temperature = 21.5; + this->mode = climate::CLIMATE_MODE_AUTO; + this->action = climate::CLIMATE_ACTION_COOLING; + this->fan_mode = climate::CLIMATE_FAN_HIGH; + this->custom_preset = {"My Preset"}; + break; + case DemoClimateType::TYPE_3: + this->current_temperature = 21.5; + this->target_temperature_low = 21.0; + this->target_temperature_high = 22.5; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + this->custom_fan_mode = {"Auto Low"}; + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + this->preset = climate::CLIMATE_PRESET_AWAY; + break; + } + this->publish_state(); + } + + protected: + void control(const climate::ClimateCall &call) override { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + } + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + } + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + } + if (call.get_fan_mode().has_value()) { + this->fan_mode = *call.get_fan_mode(); + this->custom_fan_mode.reset(); + } + if (call.get_swing_mode().has_value()) { + this->swing_mode = *call.get_swing_mode(); + } + if (call.get_custom_fan_mode().has_value()) { + this->custom_fan_mode = *call.get_custom_fan_mode(); + this->fan_mode.reset(); + } + if (call.get_preset().has_value()) { + this->preset = *call.get_preset(); + this->custom_preset.reset(); + } + if (call.get_custom_preset().has_value()) { + this->custom_preset = *call.get_custom_preset(); + this->preset.reset(); + } + this->publish_state(); + } + climate::ClimateTraits traits() override { + climate::ClimateTraits traits{}; + switch (type_) { + case DemoClimateType::TYPE_1: + traits.set_supports_current_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + }); + traits.set_supports_action(true); + traits.set_visual_temperature_step(0.5); + break; + case DemoClimateType::TYPE_2: + traits.set_supports_current_temperature(false); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_AUTO, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + }); + traits.set_supports_action(true); + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_ON, + climate::CLIMATE_FAN_OFF, + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_FOCUS, + climate::CLIMATE_FAN_DIFFUSE, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_custom_presets({"My Preset"}); + break; + case DemoClimateType::TYPE_3: + traits.set_supports_current_temperature(true); + traits.set_supports_two_point_target_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_HEAT_COOL, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_COMFORT, + climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_SLEEP, + climate::CLIMATE_PRESET_ACTIVITY, + }); + break; + } + return traits; + } + + DemoClimateType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h new file mode 100644 index 0000000000..ab039736fb --- /dev/null +++ b/esphome/components/demo/demo_cover.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace demo { + +enum class DemoCoverType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoCover : public cover::Cover, public Component { + public: + void set_type(DemoCoverType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoCoverType::TYPE_1: + this->position = cover::COVER_OPEN; + break; + case DemoCoverType::TYPE_2: + this->position = 0.7; + break; + case DemoCoverType::TYPE_3: + this->position = 0.1; + this->tilt = 0.8; + break; + case DemoCoverType::TYPE_4: + this->position = cover::COVER_CLOSED; + this->tilt = 1.0; + break; + } + this->publish_state(); + } + + protected: + void control(const cover::CoverCall &call) override { + if (call.get_position().has_value()) { + float target = *call.get_position(); + this->current_operation = + target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; + + this->set_timeout("move", 2000, [this, target]() { + this->current_operation = cover::COVER_OPERATION_IDLE; + this->position = target; + this->publish_state(); + }); + } + if (call.get_tilt().has_value()) { + this->tilt = *call.get_tilt(); + } + if (call.get_stop()) { + this->cancel_timeout("move"); + } + + this->publish_state(); + } + cover::CoverTraits get_traits() override { + cover::CoverTraits traits{}; + switch (type_) { + case DemoCoverType::TYPE_1: + traits.set_is_assumed_state(true); + break; + case DemoCoverType::TYPE_2: + traits.set_supports_position(true); + break; + case DemoCoverType::TYPE_3: + traits.set_supports_position(true); + traits.set_supports_tilt(true); + break; + case DemoCoverType::TYPE_4: + traits.set_is_assumed_state(true); + traits.set_supports_tilt(true); + break; + } + return traits; + } + + DemoCoverType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h new file mode 100644 index 0000000000..e926f68edb --- /dev/null +++ b/esphome/components/demo/demo_fan.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace demo { + +enum class DemoFanType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoFan : public Component { + public: + void set_type(DemoFanType type) { type_ = type; } + void set_fan(fan::FanState *fan) { fan_ = fan; } + void setup() override { + fan::FanTraits traits{}; + + // oscillation + // speed + // direction + // speed_count + switch (type_) { + case DemoFanType::TYPE_1: + break; + case DemoFanType::TYPE_2: + traits.set_oscillation(true); + break; + case DemoFanType::TYPE_3: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(5); + break; + case DemoFanType::TYPE_4: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(100); + traits.set_oscillation(true); + break; + } + + this->fan_->set_traits(traits); + } + + fan::FanState *fan_; + DemoFanType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h new file mode 100644 index 0000000000..2007e9ff50 --- /dev/null +++ b/esphome/components/demo/demo_light.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace demo { + +enum class DemoLightType { + // binary + TYPE_1, + // brightness + TYPE_2, + // RGB + TYPE_3, + // RGBW + TYPE_4, + // RGBWW + TYPE_5, + // CWWW + TYPE_6, + // RGBW + color_interlock + TYPE_7, +}; + +class DemoLight : public light::LightOutput, public Component { + public: + void set_type(DemoLightType type) { type_ = type; } + light::LightTraits get_traits() override { + light::LightTraits traits{}; + switch (type_) { + case DemoLightType::TYPE_1: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + break; + case DemoLightType::TYPE_2: + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + break; + case DemoLightType::TYPE_3: + traits.set_supported_color_modes({light::ColorMode::RGB}); + break; + case DemoLightType::TYPE_4: + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + break; + case DemoLightType::TYPE_5: + traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_6: + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_7: + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + break; + } + return traits; + } + void write_state(light::LightState *state) override { + // do nothing + } + + DemoLightType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_number.h b/esphome/components/demo/demo_number.h new file mode 100644 index 0000000000..2ce3a269bc --- /dev/null +++ b/esphome/components/demo/demo_number.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace demo { + +enum class DemoNumberType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoNumber : public number::Number, public Component { + public: + void set_type(DemoNumberType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoNumberType::TYPE_1: + this->publish_state(50); + break; + case DemoNumberType::TYPE_2: + this->publish_state(-10); + break; + case DemoNumberType::TYPE_3: + this->publish_state(42); + break; + } + } + + protected: + void control(float value) override { this->publish_state(value); } + + DemoNumberType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h new file mode 100644 index 0000000000..b4afa03e11 --- /dev/null +++ b/esphome/components/demo/demo_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace demo { + +class DemoSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + bool increasing = this->get_state_class() == sensor::STATE_CLASS_TOTAL_INCREASING; + if (increasing) { + float base = std::isnan(this->state) ? 0.0f : this->state; + this->publish_state(base + val * 10); + } else { + if (val < 0.1) + this->publish_state(NAN); + else + this->publish_state(val * 100); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_switch.h b/esphome/components/demo/demo_switch.h new file mode 100644 index 0000000000..9c291318ca --- /dev/null +++ b/esphome/components/demo/demo_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace demo { + +class DemoSwitch : public switch_::Switch, public Component { + public: + void setup() override { + bool initial = random_float() < 0.5; + this->publish_state(initial); + } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_text_sensor.h b/esphome/components/demo/demo_text_sensor.h new file mode 100644 index 0000000000..b4152fc248 --- /dev/null +++ b/esphome/components/demo/demo_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace demo { + +class DemoTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + if (val < 0.33) { + this->publish_state("foo"); + } else if (val < 0.66) { + this->publish_state("bar"); + } else { + this->publish_state("foobar"); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 6af83888ab..3cdfc8ab85 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -68,6 +68,9 @@ CONFIG_SCHEMA = cv.All( } ).extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "dfplayer", baud_rate=9600, require_tx=True +) async def to_code(config): @@ -80,12 +83,6 @@ async def to_code(config): await automation.build_automation(trigger, [], conf) -def validate(config, item_config): - uart.validate_device( - "dfplayer", config, item_config, baud_rate=9600, require_rx=False - ) - - @automation.register_action( "dfplayer.play_next", NextAction, diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 5cd49c311d..ae47cb33f1 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -116,7 +116,7 @@ DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto file = this->file_.value(x...); @@ -133,7 +133,7 @@ template class PlayFolderAction : public Action, public P public: TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto folder = this->folder_.value(x...); diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index fd51a976b7..2a4ccf1529 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -71,7 +71,7 @@ void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; } -bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool report_errors) { +bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool report_errors) { *humidity = NAN; *temperature = NAN; @@ -79,26 +79,31 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, int8_t i = 0; uint8_t data[5] = {0, 0, 0, 0, 0}; + this->pin_->digital_write(false); + this->pin_->pin_mode(gpio::FLAG_OUTPUT); + this->pin_->digital_write(false); + + if (this->model_ == DHT_MODEL_DHT11) { + delayMicroseconds(18000); + } else if (this->model_ == DHT_MODEL_SI7021) { + delayMicroseconds(500); + this->pin_->digital_write(true); + delayMicroseconds(40); + } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { + delayMicroseconds(2000); + } else if (this->model_ == DHT_MODEL_AM2302) { + delayMicroseconds(1000); + } else { + delayMicroseconds(800); + } + this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + { InterruptLock lock; - - this->pin_->digital_write(false); - this->pin_->pin_mode(OUTPUT); - this->pin_->digital_write(false); - - if (this->model_ == DHT_MODEL_DHT11) { - delayMicroseconds(18000); - } else if (this->model_ == DHT_MODEL_SI7021) { - delayMicroseconds(500); - this->pin_->digital_write(true); - delayMicroseconds(40); - } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { - delayMicroseconds(2000); - } else { - delayMicroseconds(800); - } - this->pin_->pin_mode(INPUT_PULLUP); - delayMicroseconds(40); + // Host pull up 20-40us then DHT response 80us + // Start waiting for initial rising edge at the center when we + // expect the DHT response (30us+40us) + delayMicroseconds(70); uint8_t bit = 7; uint8_t byte = 0; @@ -201,7 +206,7 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, const uint16_t raw_humidity = uint16_t(data[0]) * 10 + data[1]; *humidity = raw_humidity / 10.0f; } else { - // For compatibily with DHT11 models which might only use 2 bytes checksums, only use the data from these two + // For compatibility with DHT11 models which might only use 2 bytes checksums, only use the data from these two // bytes *temperature = data[2]; *humidity = data[0]; diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 8826a3d2b7..f3a29f9ce9 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -35,7 +36,7 @@ class DHT : public PollingComponent { */ void set_dht_model(DHTModel model); - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_model(DHTModel model) { model_ = model; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -51,7 +52,7 @@ class DHT : public PollingComponent { protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); - GPIOPin *pin_; + InternalGPIOPin *pin_; DHTModel model_{DHT_MODEL_AUTO_DETECT}; bool is_auto_detect_{false}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index c33ddd2286..1334f0270c 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -36,14 +35,16 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(DHT), cv.Required(CONF_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MODEL, default="auto detect"): cv.enum( DHT_MODELS, upper=True, space="_" diff --git a/esphome/components/dht12/sensor.py b/esphome/components/dht12/sensor.py index 14c01f5d34..ae2173ef22 100644 --- a/esphome/components/dht12/sensor.py +++ b/esphome/components/dht12/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(DHT12Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 2dff00da03..947b09a258 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -31,7 +31,9 @@ DisplayPageShowPrevAction = display_ns.class_( DisplayIsDisplayingPageCondition = display_ns.class_( "DisplayIsDisplayingPageCondition", automation.Condition ) -DisplayOnPageChangeTrigger = display_ns.class_("DisplayOnPageChangeTrigger") +DisplayOnPageChangeTrigger = display_ns.class_( + "DisplayOnPageChangeTrigger", automation.Trigger +) CONF_ON_PAGE_CHANGE = "on_page_change" diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 08050b2078..2ee06e379f 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,9 +1,10 @@ #include "display_buffer.h" +#include #include "esphome/core/application.h" #include "esphome/core/color.h" #include "esphome/core/log.h" -#include +#include "esphome/core/hal.h" namespace esphome { namespace display { @@ -14,7 +15,7 @@ const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void DisplayBuffer::init_internal_(uint32_t buffer_length) { - this->buffer_ = new uint8_t[buffer_length]; + this->buffer_ = new (std::nothrow) uint8_t[buffer_length]; // NOLINT if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); return; @@ -233,6 +234,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo } } +#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 + 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; @@ -365,7 +373,7 @@ bool Glyph::get_pixel(int x, int y) const { 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 pgm_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); + 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 { @@ -457,22 +465,22 @@ bool Image::get_pixel(int x, int y) const { return false; const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t pos = x + y * width_8; - return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); + return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } Color Image::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_) * 3; - const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | - (pgm_read_byte(this->data_start_ + pos + 1) << 8) | - (pgm_read_byte(this->data_start_ + pos + 0) << 16); + const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | + (progmem_read_byte(this->data_start_ + pos + 1) << 8) | + (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_); - const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); return Color(gray | gray << 8 | gray << 16 | gray << 24); } int Image::get_width() const { return this->width_; } @@ -489,34 +497,32 @@ bool Animation::get_pixel(int x, int y) const { if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) return false; const uint32_t pos = x + y * width_8 + frame_index; - return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); + return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } Color Animation::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | - (pgm_read_byte(this->data_start_ + pos + 1) << 8) | - (pgm_read_byte(this->data_start_ + pos + 0) << 16); + const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | + (progmem_read_byte(this->data_start_ + pos + 1) << 8) | + (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } Color Animation::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index); - const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); return Color(gray | gray << 8 | gray << 16 | gray << 24); } Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) - : Image(data_start, width, height, type), animation_frame_count_(animation_frame_count) { - current_frame_ = 0; -} + : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} int 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() { diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b89eab0dba..54488f18f7 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -4,11 +4,16 @@ #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "display_color_utils.h" +#include #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" #endif +#ifdef USE_GRAPH +#include "esphome/components/graph/graph.h" +#endif + namespace esphome { namespace display { @@ -273,6 +278,30 @@ class DisplayBuffer { */ 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 + /** 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. diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py new file mode 100644 index 0000000000..dd6e6051aa --- /dev/null +++ b/esphome/components/dsmr/__init__.py @@ -0,0 +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_UART_ID, +) + +CODEOWNERS = ["@glmnet", "@zuidwijk"] + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_DSMR_ID = "dsmr_id" +CONF_DECRYPTION_KEY = "decryption_key" +CONF_CRC_CHECK = "crc_check" +CONF_GAS_MBUS_ID = "gas_mbus_id" + +# Hack to prevent compile error due to ambiguity with lib namespace +dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") +Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) + + +def _validate_key(value): + value = cv.string_strict(value) + parts = [value[i : i + 2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise cv.Invalid("Decryption key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + # pylint: disable=raise-missing-from + raise cv.Invalid("Decryption key must be hex values from 00 to FF") + + return "".join(f"{part:02X}" for part in parts_int) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Dsmr), + 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_, + } + ).extend(uart.UART_DEVICE_SCHEMA), + cv.only_with_arduino, +) + + +async def to_code(config): + uart_component = await cg.get_variable(config[CONF_UART_ID]) + var = cg.new_Pvariable(config[CONF_ID], uart_component, config[CONF_CRC_CHECK]) + if CONF_DECRYPTION_KEY in config: + cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) + await cg.register_component(var, config) + + cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) + + # DSMR Parser + cg.add_library("glmnet/Dsmr", "0.5") + + # Crypto + cg.add_library("rweather/Crypto", "0.2.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp new file mode 100644 index 0000000000..b798fe5d44 --- /dev/null +++ b/esphome/components/dsmr/dsmr.cpp @@ -0,0 +1,200 @@ +#ifdef USE_ARDUINO + +#include "dsmr.h" +#include "esphome/core/log.h" + +#include +#include +#include + +namespace esphome { +namespace dsmr { + +static const char *const TAG = "dsmr"; + +void Dsmr::loop() { + if (this->decryption_key_.empty()) + this->receive_telegram_(); + else + this->receive_encrypted_(); +} + +void Dsmr::receive_telegram_() { + int count = MAX_BYTES_PER_LOOP; + while (available() && count-- > 0) { + const char c = read(); + + // Find a new telegram header, i.e. forward slash. + if (c == '/') { + ESP_LOGV(TAG, "Header found"); + header_found_ = true; + footer_found_ = false; + telegram_len_ = 0; + } + if (!header_found_) + continue; + + // Check for buffer overflow. + if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { + header_found_ = false; + footer_found_ = false; + ESP_LOGE(TAG, "Error: Message larger than buffer"); + return; + } + + // Some v2.2 or v3 meters will send a new value which starts with '(' + // in a new line while the value belongs to the previous ObisId. For + // proper parsing remove these new line characters + while (c == '(' && (telegram_[telegram_len_ - 1] == '\n' || telegram_[telegram_len_ - 1] == '\r')) + telegram_len_--; + + // Store the byte in the buffer. + telegram_[telegram_len_] = c; + telegram_len_++; + + // Check for a footer, i.e. exlamation mark, followed by a hex checksum. + if (c == '!') { + ESP_LOGV(TAG, "Footer found"); + footer_found_ = true; + continue; + } + // Check for the end of the hex checksum, i.e. a newline. + if (footer_found_ && c == '\n') { + header_found_ = false; + // Parse the telegram and publish sensor values. + if (parse_telegram()) + return; + } + } +} + +void Dsmr::receive_encrypted_() { + // Encrypted buffer + uint8_t buffer[MAX_TELEGRAM_LENGTH]; + size_t buffer_length = 0; + + size_t packet_size = 0; + while (available()) { + const char c = read(); + + if (!header_found_) { + if ((uint8_t) c == 0xdb) { + ESP_LOGV(TAG, "Start byte 0xDB found"); + header_found_ = true; + } + } + + // Sanity check + if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { + if (buffer_length == 0) { + ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); + } else { + ESP_LOGW(TAG, "Unexpected data"); + } + this->status_momentary_warning("unexpected_data"); + this->flush(); + while (available()) + read(); + return; + } + + buffer[buffer_length++] = c; + + if (packet_size == 0 && buffer_length > 20) { + // Complete header + a few bytes of data + packet_size = buffer[11] << 8 | buffer[12]; + } + if (buffer_length == packet_size + 13 && packet_size > 0) { + ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); + + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + buffer[i] = buffer[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&buffer[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &buffer[18], + // cipher size + buffer_length - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); + ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); + ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); + + parse_telegram(); + telegram_len_ = 0; + return; + } + + if (!available()) { + // baud rate is 115200 for encrypted data, this means a few byte should arrive every time + // program runs faster than buffer loading then available() might return false in the middle + delay(4); // Wait for data + } + } + if (buffer_length > 0) { + ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); + } +} + +bool Dsmr::parse_telegram() { + MyData data; + ESP_LOGV(TAG, "Trying to parse"); + ::dsmr::ParseResult res = + ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, false, + this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. + if (res.err) { + // Parsing error, show it + auto err_str = res.fullError(telegram_, telegram_ + telegram_len_); + ESP_LOGE(TAG, "%s", err_str.c_str()); + return false; + } else { + this->status_clear_warning(); + publish_sensors(data); + return true; + } +} + +void Dsmr::dump_config() { + ESP_LOGCONFIG(TAG, "dsmr:"); + +#define DSMR_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s_##s##_); + DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) + +#define DSMR_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s_##s##_); + DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) +} + +void Dsmr::set_decryption_key(const std::string &decryption_key) { + if (decryption_key.length() == 0) { + ESP_LOGI(TAG, "Disabling decryption"); + this->decryption_key_.clear(); + return; + } + + if (decryption_key.length() != 32) { + ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); + return; + } + this->decryption_key_.clear(); + + ESP_LOGI(TAG, "Decryption key is set."); + // Verbose level prints decryption key + ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); + + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); + decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); + } +} + +} // namespace dsmr +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h new file mode 100644 index 0000000000..4f9a66b3d0 --- /dev/null +++ b/esphome/components/dsmr/dsmr.h @@ -0,0 +1,110 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +// don't include because it puts everything in global namespace +#include +#include + +namespace esphome { +namespace dsmr { + +static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; +static constexpr uint32_t MAX_BYTES_PER_LOOP = 50; +static constexpr uint32_t POLL_TIMEOUT = 1000; + +using namespace ::dsmr::fields; + +// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines + +#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST) +// Neither set, set it to a dummy value to not break build +#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification) +#endif + +#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST) +#define DSMR_BOTH , +#else +#define DSMR_BOTH +#endif + +#ifndef DSMR_SENSOR_LIST +#define DSMR_SENSOR_LIST(F, SEP) +#endif + +#ifndef DSMR_TEXT_SENSOR_LIST +#define DSMR_TEXT_SENSOR_LIST(F, SEP) +#endif + +#define DSMR_DATA_SENSOR(s) s +#define DSMR_COMMA , + +using MyData = ::dsmr::ParsedData; + +class Dsmr : public Component, public uart::UARTDevice { + public: + Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {} + + void loop() override; + + bool parse_telegram(); + + void publish_sensors(MyData &data) { +#define DSMR_PUBLISH_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s); + DSMR_SENSOR_LIST(DSMR_PUBLISH_SENSOR, ) + +#define DSMR_PUBLISH_TEXT_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s.c_str()); + DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) + }; + + void dump_config() override; + + void set_decryption_key(const std::string &decryption_key); + +// Sensor setters +#define DSMR_SET_SENSOR(s) \ + void set_##s(sensor::Sensor *sensor) { s_##s##_ = sensor; } + DSMR_SENSOR_LIST(DSMR_SET_SENSOR, ) + +#define DSMR_SET_TEXT_SENSOR(s) \ + void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } + DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + + protected: + void receive_telegram_(); + void receive_encrypted_(); + + // Telegram buffer + char telegram_[MAX_TELEGRAM_LENGTH]; + int telegram_len_{0}; + + // Serial parser + bool header_found_{false}; + bool footer_found_{false}; + +// Sensor member pointers +#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; + DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) + +#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr}; + DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) + + std::vector decryption_key_{}; + bool crc_check_; +}; +} // namespace dsmr +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py new file mode 100644 index 0000000000..761009c766 --- /dev/null +++ b/esphome/components/dsmr/sensor.py @@ -0,0 +1,250 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CUBIC_METER, + UNIT_EMPTY, + UNIT_KILOWATT, + UNIT_KILOWATT_HOURS, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOVOLT_AMPS_REACTIVE, + UNIT_VOLT, +) +from . import Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("energy_delivered_lux"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_lux"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("total_imported_energy"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional("total_exported_energy"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional("power_delivered"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("electricity_threshold"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_switch_position"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_long_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("current_l1"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l2"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l3"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l1"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l2"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l3"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l1"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l2"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l3"): sensor.sensor_schema( + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_l1"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l2"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l3"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("gas_delivered"): sensor.sensor_schema( + UNIT_CUBIC_METER, + ICON_EMPTY, + 3, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("gas_delivered_be"): sensor.sensor_schema( + UNIT_CUBIC_METER, + ICON_EMPTY, + 3, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL_INCREASING, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == sensor.Sensor: + s = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}")(s)) + sensors.append(f"F({key})") + + if sensors: + cg.add_define( + "DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors)) + ) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py new file mode 100644 index 0000000000..339eea711f --- /dev/null +++ b/esphome/components/dsmr/text_sensor.py @@ -0,0 +1,101 @@ +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 Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("gas_delivered_text"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + text_sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == text_sensor.TextSensor: + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + cg.add(getattr(hub, f"set_{key}")(var)) + text_sensors.append(f"F({key})") + + if text_sensors: + cg.add_define( + "DSMR_TEXT_SENSOR_LIST(F, sep)", + cg.RawExpression(" sep ".join(text_sensors)), + ) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 8b7446b681..3d7f731d5d 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -13,8 +13,9 @@ void DutyCycleSensor::setup() { this->store_.pin = this->pin_->to_isr(); this->store_.last_level = this->pin_->digital_read(); this->last_update_ = micros(); + this->store_.last_interrupt = micros(); - this->pin_->attach_interrupt(DutyCycleSensorStore::gpio_intr, &this->store_, CHANGE); + this->pin_->attach_interrupt(DutyCycleSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } void DutyCycleSensor::dump_config() { LOG_SENSOR("", "Duty Cycle Sensor", this); @@ -43,8 +44,8 @@ void DutyCycleSensor::update() { float DutyCycleSensor::get_setup_priority() const { return setup_priority::DATA; } -void ICACHE_RAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { - const bool new_level = arg->pin->digital_read(); +void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { + const bool new_level = arg->pin.digital_read(); if (new_level == arg->last_level) return; arg->last_level = new_level; diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index 2205bec729..22d3588fb7 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -12,14 +12,14 @@ struct DutyCycleSensorStore { volatile uint32_t last_interrupt{0}; volatile uint32_t on_time{0}; volatile bool last_level{false}; - ISRInternalGPIOPin *pin; + ISRInternalGPIOPin pin; static void gpio_intr(DutyCycleSensorStore *arg); }; class DutyCycleSensor : public sensor::Sensor, public PollingComponent { public: - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override; float get_setup_priority() const override; @@ -27,9 +27,9 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void update() override; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; - DutyCycleSensorStore store_; + DutyCycleSensorStore store_{}; uint32_t last_update_; }; diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 39f6ebc88f..6a367328e6 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_PERCENT, @@ -18,14 +17,15 @@ DutyCycleSensor = duty_cycle_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_PERCENT, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { cv.GenerateID(): cv.declare_id(DutyCycleSensor), - cv.Required(CONF_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } ) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index 5eb823064d..bb662e0989 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -4,6 +4,8 @@ from esphome.components.light.types import AddressableLightEffect from esphome.components.light.effects import register_addressable_effect from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS +DEPENDENCIES = ["network"] + e131_ns = cg.esphome_ns.namespace("e131") E131AddressableLightEffect = e131_ns.class_( "E131AddressableLightEffect", AddressableLightEffect @@ -21,11 +23,16 @@ CHANNELS = { CONF_UNIVERSE = "universe" CONF_E131_ID = "e131_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(E131Component), - cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), - } +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of( + *METHODS, upper=True + ), + } + ), + cv.only_with_arduino, ) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index fb72e5b470..35510fe204 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,12 +1,14 @@ +#ifdef USE_ARDUINO + #include "e131.h" #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #include #endif @@ -26,7 +28,7 @@ E131Component::~E131Component() { } void E131Component::setup() { - udp_.reset(new WiFiUDP()); + udp_ = make_unique(); if (!udp_->begin(PORT)) { ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); @@ -50,7 +52,7 @@ void E131Component::loop() { } if (!packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); continue; } @@ -104,3 +106,5 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 3f647edbf1..3819e522a5 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include @@ -55,3 +57,5 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index f280b5bc94..371f3b9cbf 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "e131.h" #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" @@ -84,8 +86,11 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet break; } + it->schedule_show(); return true; } } // namespace e131 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 1ab5d43164..e78f6bb0e0 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" @@ -46,3 +48,5 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index 14fdc084a6..b20eb9f666 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,8 +1,14 @@ +#ifdef USE_ARDUINO + #include "e131.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#include "esphome/components/network/ip_address.h" +#include +#include #include +#include #include namespace esphome { @@ -63,8 +69,8 @@ bool E131Component::join_igmp_groups_() { if (!universe.second) continue; - ip4_addr_t multicast_addr = { - static_cast(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; + ip4_addr_t multicast_addr = {static_cast( + network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); @@ -98,7 +104,7 @@ void E131Component::leave_(int universe) { if (listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = { - static_cast(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + static_cast(network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); } @@ -134,3 +140,5 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index cbc4b334d9..67c6a4ebd3 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -1,5 +1,6 @@ #include "endstop_cover.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace endstop { diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py new file mode 100644 index 0000000000..704f9bb3e8 --- /dev/null +++ b/esphome/components/esp32/__init__.py @@ -0,0 +1,395 @@ +from dataclasses import dataclass +from typing import Union +from pathlib import Path +import logging + +from esphome.helpers import write_file_if_changed +from esphome.const import ( + CONF_BOARD, + CONF_FRAMEWORK, + CONF_TYPE, + CONF_VARIANT, + CONF_VERSION, + CONF_ADVANCED, + CONF_IGNORE_EFUSE_MAC_CRC, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE, HexInt +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import ( + KEY_BOARD, + KEY_ESP32, + KEY_SDKCONFIG_OPTIONS, + KEY_VARIANT, + VARIANT_ESP32C3, + VARIANTS, +) + +# 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"] + + +def set_core_data(config): + CORE.data[KEY_ESP32] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp32" + conf = config[CONF_FRAMEWORK] + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf" + CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} + elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + config[CONF_FRAMEWORK][CONF_VERSION_HINT] + ) + CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] + return config + + +def get_esp32_variant(): + return CORE.data[KEY_ESP32][KEY_VARIANT] + + +def is_esp32c3(): + return get_esp32_variant() == VARIANT_ESP32C3 + + +@dataclass +class RawSdkconfigValue: + """An sdkconfig value that won't be auto-formatted""" + + value: str + + +SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue] + + +def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): + """Set an esp-idf sdkconfig value.""" + if not CORE.using_esp_idf: + raise ValueError("Not an esp-idf project") + CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value + + +def _format_framework_arduino_version(ver: cv.Version) -> str: + # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to + # a PIO platformio/framework-arduinoespressif32 value + # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 + if ver <= cv.Version(1, 0, 3): + return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + +# NOTE: Keep this in mind when updating the recommended version: +# * New framework historically have had some regressions, especially for WiFi. +# The new version needs to be thoroughly validated before changing the +# recommended version as otherwise a bunch of devices could be bricked +# * For all constants below, update platformio.ini (in this repo) +# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository + +# The default/recommended arduino framework version +# - https://github.com/espressif/arduino-esp32/releases +# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(1, 0, 6) +# 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(3, 3, 2) + +# 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, 3, 0) +# 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(3, 3, 2) + + +def _arduino_check_versions(value): + value = value.copy() + lookups = { + "dev": ("https://github.com/espressif/arduino-esp32.git", cv.Version(2, 0, 0)), + "latest": ("", cv.Version(1, 0, 3)), + "recommended": ( + _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), + RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, + ), + } + ver_value = value[CONF_VERSION] + default_ver_hint = None + if ver_value.lower() in lookups: + default_ver_hint = str(lookups[ver_value.lower()][1]) + ver_value = lookups[ver_value.lower()][0] + else: + with cv.suppress_invalid(): + ver = cv.Version.parse(cv.version_number(value)) + if ver <= cv.Version(1, 0, 3): + ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + else: + ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + default_ver_hint = str(ver) + value[CONF_VERSION] = ver_value + + if CONF_VERSION_HINT not in value and default_ver_hint is None: + raise cv.Invalid("Needs a version hint to understand the framework version") + + ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) + value[CONF_VERSION_HINT] = ver_hint_s + plat_ver = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) + value[CONF_PLATFORM_VERSION] = str(plat_ver) + + if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected arduino framework version is not the recommended one" + ) + _LOGGER.warning( + "If there are connectivity or build issues please remove the manual version" + ) + + return value + + +def _format_framework_espidf_version(ver: cv.Version) -> str: + # format the given arduino (https://github.com/espressif/esp-idf/releases) version to + # a PIO platformio/framework-espidf value + # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf + return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + +def _esp_idf_check_versions(value): + value = value.copy() + lookups = { + "dev": ("https://github.com/espressif/esp-idf.git", cv.Version(4, 3, 1)), + "latest": ("", cv.Version(4, 3, 0)), + "recommended": ( + _format_framework_espidf_version(RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION), + RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, + ), + } + ver_value = value[CONF_VERSION] + default_ver_hint = None + if ver_value.lower() in lookups: + default_ver_hint = str(lookups[ver_value.lower()][1]) + ver_value = lookups[ver_value.lower()][0] + else: + with cv.suppress_invalid(): + ver = cv.Version.parse(cv.version_number(value)) + ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + default_ver_hint = str(ver) + value[CONF_VERSION] = ver_value + + if CONF_VERSION_HINT not in value and default_ver_hint is None: + raise cv.Invalid("Needs a version hint to understand the framework version") + + ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) + value[CONF_VERSION_HINT] = ver_hint_s + if cv.Version.parse(ver_hint_s) < cv.Version(4, 0, 0): + raise cv.Invalid("Only ESP-IDF 4.0+ is supported") + if cv.Version.parse(ver_hint_s) != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected esp-idf framework version is not the recommended one" + ) + _LOGGER.warning( + "If there are connectivity or build issues please remove the manual version" + ) + + plat_ver = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) + value[CONF_PLATFORM_VERSION] = str(plat_ver) + + return value + + +CONF_VERSION_HINT = "version_hint" +CONF_PLATFORM_VERSION = "platform_version" +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + } + ), + _arduino_check_versions, +) +CONF_SDKCONFIG_OPTIONS = "sdkconfig_options" +ESP_IDF_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + } + ), + } + ), + _esp_idf_check_versions, +) + + +FRAMEWORK_ESP_IDF = "esp-idf" +FRAMEWORK_ARDUINO = "arduino" +FRAMEWORK_SCHEMA = cv.typed_schema( + { + FRAMEWORK_ESP_IDF: ESP_IDF_FRAMEWORK_SCHEMA, + FRAMEWORK_ARDUINO: ARDUINO_FRAMEWORK_SCHEMA, + }, + lower=True, + space="-", + default_type=FRAMEWORK_ARDUINO, +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_VARIANT, default="ESP32"): cv.one_of( + *VARIANTS, upper=True + ), + cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, + } + ), + set_core_data, +) + + +async def to_code(config): + cg.add_platformio_option("board", config[CONF_BOARD]) + 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]}") + + cg.add_platformio_option("lib_ldf_mode", "off") + + conf = config[CONF_FRAMEWORK] + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + cg.add_platformio_option( + "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" + ) + cg.add_platformio_option("framework", "espidf") + cg.add_build_flag("-DUSE_ESP_IDF") + cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") + cg.add_build_flag("-Wno-nonnull-compare") + cg.add_platformio_option( + "platform_packages", + [f"platformio/framework-espidf @ {conf[CONF_VERSION]}"], + ) + add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) + add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) + add_idf_sdkconfig_option( + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv" + ) + add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) + add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_SIZE", True) + + cg.add_platformio_option("board_build.partitions", "partitions.csv") + + 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") + add_idf_sdkconfig_option( + "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) + + elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + cg.add_platformio_option( + "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" + ) + cg.add_platformio_option("framework", "arduino") + cg.add_build_flag("-DUSE_ARDUINO") + cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") + cg.add_platformio_option( + "platform_packages", + [f"platformio/framework-arduinoespressif32 @ {conf[CONF_VERSION]}"], + ) + + cg.add_platformio_option("board_build.partitions", "partitions.csv") + + +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 +""" + + +IDF_PARTITIONS_CSV = """\ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, , 0x4000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +app0, app, ota_0, , 0x1C0000, +app1, app, ota_1, , 0x1C0000, +""" + + +def _format_sdkconfig_val(value: SdkconfigValueType) -> str: + if isinstance(value, bool): + return "y" if value else "n" + if isinstance(value, int): + return str(value) + if isinstance(value, str): + return f'"{value}"' + if isinstance(value, RawSdkconfigValue): + return value.value + raise ValueError + + +def _write_sdkconfig(): + # sdkconfig.{name} stores the real sdkconfig (modified by esp-idf with default) + # sdkconfig.{name}.esphomeinternal stores what esphome last wrote + # we use the internal one to detect if there were any changes, and if so write them to the + # real sdkconfig + sdk_path = Path(CORE.relative_build_path(f"sdkconfig.{CORE.name}")) + internal_path = Path( + CORE.relative_build_path(f"sdkconfig.{CORE.name}.esphomeinternal") + ) + + want_opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + contents = ( + "\n".join( + f"{name}={_format_sdkconfig_val(value)}" + for name, value in sorted(want_opts.items()) + ) + + "\n" + ) + if write_file_if_changed(internal_path, contents): + # internal changed, update real one + write_file_if_changed(sdk_path, contents) + + +# 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 CORE.using_esp_idf: + _write_sdkconfig() + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + IDF_PARTITIONS_CSV, + ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py new file mode 100644 index 0000000000..ddf4bf2026 --- /dev/null +++ b/esphome/components/esp32/boards.py @@ -0,0 +1,927 @@ +ESP32_BASE_PINS = { + "TX": 1, + "RX": 3, + "SDA": 21, + "SCL": 22, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "A0": 36, + "A3": 39, + "A4": 32, + "A5": 33, + "A6": 34, + "A7": 35, + "A10": 4, + "A11": 0, + "A12": 2, + "A13": 15, + "A14": 13, + "A15": 12, + "A16": 14, + "A17": 27, + "A18": 25, + "A19": 26, + "T0": 4, + "T1": 0, + "T2": 2, + "T3": 15, + "T4": 13, + "T5": 12, + "T6": 14, + "T7": 27, + "T8": 33, + "T9": 32, + "DAC1": 25, + "DAC2": 26, + "SVP": 36, + "SVN": 39, +} + +ESP32_BOARD_PINS = { + "alksesp32": { + "A0": 32, + "A1": 33, + "A2": 25, + "A3": 26, + "A4": 27, + "A5": 14, + "A6": 12, + "A7": 15, + "D0": 40, + "D1": 41, + "D10": 19, + "D11": 21, + "D12": 22, + "D13": 23, + "D2": 15, + "D3": 2, + "D4": 0, + "D5": 4, + "D6": 16, + "D7": 17, + "D8": 5, + "D9": 18, + "DHT_PIN": 26, + "LED": 23, + "L_B": 5, + "L_G": 17, + "L_R": 22, + "L_RGB_B": 16, + "L_RGB_G": 21, + "L_RGB_R": 4, + "L_Y": 23, + "MISO": 22, + "MOSI": 21, + "PHOTO": 25, + "PIEZO1": 19, + "PIEZO2": 18, + "POT1": 32, + "POT2": 33, + "S1": 4, + "S2": 16, + "S3": 18, + "S4": 19, + "S5": 21, + "SCK": 23, + "SCL": 14, + "SDA": 27, + "SS": 19, + "SW1": 15, + "SW2": 2, + "SW3": 0, + }, + "az-delivery-devkit-v4": {}, + "bpi-bit": { + "BUTTON_A": 35, + "BUTTON_B": 27, + "BUZZER": 25, + "LIGHT_SENSOR1": 36, + "LIGHT_SENSOR2": 39, + "MPU9250_INT": 0, + "P0": 25, + "P1": 32, + "P10": 26, + "P11": 27, + "P12": 2, + "P13": 18, + "P14": 19, + "P15": 23, + "P16": 5, + "P19": 22, + "P2": 33, + "P20": 21, + "P3": 13, + "P4": 15, + "P5": 35, + "P6": 12, + "P7": 14, + "P8": 16, + "P9": 17, + "RGB_LED": 4, + "TEMPERATURE_SENSOR": 34, + }, + "briki_abc_esp32": {}, + "briki_mbc-wb_esp32": {}, + "d-duino-32": { + "D1": 5, + "D10": 1, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 4, + "SDA": 5, + "SS": 15, + }, + "esp-wrover-kit": {}, + "esp32-devkitlipo": {}, + "esp32-evb": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, + "esp32-poe-iso": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + }, + "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, + "esp32-pro": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp320": { + "LED": 5, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 14, + "SDA": 2, + "SS": 15, + }, + "esp32cam": {}, + "esp32dev": {}, + "esp32doit-devkit-v1": {"LED": 2}, + "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8}, + "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, + "esp32thing_plus": { + "SDA": 23, + "SCL": 22, + "SS": 33, + "MOSI": 18, + "MISO": 19, + "SCK": 5, + "A0": 26, + "A1": 25, + "A2": 34, + "A3": 39, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "A10": 27, + "A11": 12, + "A12": 13, + }, + "esp32vn-iot-uno": {}, + "espea32": {"BUTTON": 0, "LED": 5}, + "espectro32": {"LED": 15, "SD_SS": 33}, + "espino32": {"BUTTON": 0, "LED": 16}, + "etboard": { + "LED_BUILTIN": 5, + "TX": 34, + "RX": 35, + "SS": 29, + "MOSI": 37, + "MISO": 31, + "SCK": 30, + "A0": 36, + "A1": 39, + "A2": 32, + "A3": 33, + "A4": 34, + "A5": 35, + "A6": 25, + "A7": 26, + "D2": 27, + "D3": 14, + "D4": 12, + "D5": 13, + "D6": 15, + "D7": 16, + "D8": 17, + "D9": 4, + }, + "featheresp32": { + "A0": 26, + "A1": 25, + "A10": 27, + "A11": 12, + "A12": 13, + "A13": 35, + "A2": 34, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "Ax": 2, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 33, + "TX": 17, + }, + "firebeetle32": {"LED": 2}, + "fm-devkit": { + "D0": 34, + "D1": 35, + "D10": 0, + "D2": 32, + "D3": 33, + "D4": 27, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 23, + "I2S_DOUT": 22, + "I2S_LRCLK": 25, + "I2S_MCLK": 2, + "I2S_SCLK": 26, + "LED": 5, + "SCL": 17, + "SDA": 16, + "SW1": 4, + "SW2": 18, + "SW3": 19, + "SW4": 21, + }, + "frogboard": {}, + "healtypi4": { + "KEY_BUILTIN": 17, + "ADS1292_DRDY_PIN": 26, + "ADS1292_CS_PIN": 13, + "ADS1292_START_PIN": 14, + "ADS1292_PWDN_PIN": 27, + "AFE4490_CS_PIN": 21, + "AFE4490_DRDY_PIN": 39, + "AFE4490_PWDN_PIN": 4, + "PUSH_BUTTON": 17, + "SLIDE_SWITCH": 16, + }, + "heltec_wifi_kit_32": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 25, + "RST_OLED": 16, + "SCL_OLED": 15, + "SDA_OLED": 4, + "Vext": 21, + }, + "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32", + "heltec_wifi_lora_32": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 33, + "DIO2": 32, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wifi_lora_32_V2": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wireless_stick": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wireless_stick_lite": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "Vext": 21, + "LED": 25, + "RST_LoRa": 14, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + }, + "honeylemon": { + "LED_BUILTIN": 2, + "BUILTIN_KEY": 0, + }, + "hornbill32dev": {"BUTTON": 0, "LED": 13}, + "hornbill32minima": {"SS": 2}, + "imbrios-logsens-v1p1": { + "LED_BUILTIN": 33, + "UART2_TX": 17, + "UART2_RX": 16, + "UART2_RTS": 4, + "CAN_TX": 17, + "CAN_RX": 16, + "CAN_TXDE": 4, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SPI_SS1": 23, + "BUZZER_CTRL": 19, + "SD_CARD_DETECT": 35, + "SW2_BUILDIN": 0, + "SW3_BUILDIN": 36, + "SW4_BUILDIN": 34, + "LED1_BUILDIN": 32, + "LED2_BUILDIN": 33, + }, + "inex_openkb": { + "LED_BUILTIN": 16, + "LDR_PIN": 36, + "SW1": 16, + "SW2": 14, + "BT_LED": 17, + "WIFI_LED": 2, + "NTP_LED": 15, + "IOT_LED": 12, + "BUZZER": 13, + "INPUT1": 32, + "INPUT2": 33, + "INPUT3": 34, + "INPUT4": 35, + "OUTPUT1": 26, + "OUTPUT2": 27, + "SDA0": 21, + "SCL0": 22, + "SDA1": 4, + "SCL1": 5, + }, + "intorobot": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 4, + "MISO": 17, + "MOSI": 16, + "RGB_B_BUILTIN": 22, + "RGB_G_BUILTIN": 21, + "RGB_R_BUILTIN": 27, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "iotaap_magnolia": {}, + "iotbusio": {}, + "iotbusproteus": {}, + "kits-edu": {}, + "labplus_mpython": { + "SDA": 23, + "SCL": 22, + "P0": 33, + "P1": 32, + "P2": 35, + "P3": 34, + "P4": 39, + "P5": 0, + "P6": 16, + "P7": 17, + "P8": 26, + "P9": 25, + "P10": 36, + "P11": 2, + "P13": 18, + "P14": 19, + "P15": 21, + "P16": 5, + "P19": 22, + "P20": 23, + "P": 27, + "Y": 14, + "T": 12, + "H": 13, + "O": 15, + "N": 4, + "BTN_A": 0, + "BTN_B": 2, + "SOUND": 36, + "LIGHT": 39, + "BUZZER": 16, + }, + "lolin32": {"LED": 5}, + "lolin32_lite": {"LED": 22}, + "lolin_d32": {"LED": 5, "_VBAT": 35}, + "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, + "lopy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "lopy4": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 18, + }, + "m5stack-atom": { + "SDA": 26, + "SCL": 32, + "ADC1": 35, + "ADC2": 36, + "SS": 19, + "MOSI": 33, + "MISO": 23, + "SCK": 22, + }, + "m5stack-core-esp32": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stack-core2": { + "SDA": 32, + "SCL": 33, + "SS": 5, + "MOSI": 23, + "MISO": 38, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, + "m5stack-coreink": { + "SDA": 32, + "SCL": 33, + "SS": 9, + "MOSI": 23, + "MISO": 34, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, + "m5stack-fire": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + }, + "m5stack-grey": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stack-timer-cam": { + "LED_BUILTIN": 2, + "SDA": 4, + "SCL": 13, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, + "m5stick-c": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G10": 10, + "G26": 26, + "G32": 32, + "G33": 33, + "G36": 36, + "G37": 37, + "G39": 39, + "G9": 9, + "MISO": 36, + "MOSI": 15, + "SCK": 13, + "SCL": 33, + "SDA": 32, + }, + "magicbit": { + "BLUE_LED": 17, + "BUZZER": 25, + "GREEN_LED": 16, + "LDR": 36, + "LED": 16, + "LEFT_BUTTON": 35, + "MOTOR1A": 27, + "MOTOR1B": 18, + "MOTOR2A": 16, + "MOTOR2B": 17, + "POT": 39, + "RED_LED": 27, + "RIGHT_PUTTON": 34, + "YELLOW_LED": 18, + }, + "mgbot-iotik32a": { + "LED_BUILTIN": 4, + "TX2": 17, + "RX2": 16, + }, + "mgbot-iotik32b": { + "LED_BUILTIN": 18, + "IR": 27, + "TX2": 17, + "RX2": 16, + }, + "mhetesp32devkit": {"LED": 2}, + "mhetesp32minikit": {"LED": 2}, + "microduino-core-esp32": { + "A0": 12, + "A1": 13, + "A10": 25, + "A11": 26, + "A12": 27, + "A13": 14, + "A2": 15, + "A3": 4, + "A6": 38, + "A7": 37, + "A8": 32, + "A9": 33, + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 23, + "D12": 19, + "D13": 18, + "D14": 12, + "D15": 13, + "D16": 15, + "D17": 4, + "D18": 22, + "D19": 21, + "D2": 16, + "D20": 38, + "D21": 37, + "D3": 17, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 27, + "D9": 14, + "SCL": 21, + "SCL1": 13, + "SDA": 22, + "SDA1": 12, + }, + "nano32": {"BUTTON": 0, "LED": 16}, + "nina_w10": { + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 19, + "D12": 23, + "D13": 18, + "D14": 13, + "D15": 12, + "D16": 32, + "D17": 33, + "D18": 21, + "D19": 34, + "D2": 26, + "D20": 36, + "D21": 39, + "D3": 25, + "D4": 35, + "D5": 27, + "D6": 22, + "D7": 0, + "D8": 15, + "D9": 14, + "LED_BLUE": 21, + "LED_GREEN": 33, + "LED_RED": 23, + "SCL": 13, + "SDA": 12, + "SW1": 33, + "SW2": 27, + }, + "node32s": {}, + "nodemcu-32s": {"BUTTON": 0, "LED": 2}, + "nscreen-32": {}, + "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, + "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, + "oroca_edubot": { + "A0": 34, + "A1": 39, + "A2": 36, + "A3": 33, + "D0": 4, + "D1": 16, + "D2": 17, + "D3": 22, + "D4": 23, + "D5": 5, + "D6": 18, + "D7": 19, + "D8": 33, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 2, + "TX": 17, + "VBAT": 35, + }, + "pico32": {}, + "piranha_esp32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + }, + "pocket_32": {"LED": 16}, + "pycom_gpy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "qchip": "heltec_wifi_kit_32", + "quantum": {}, + "s_odi_ultra": { + "LED_BUILTIN": 2, + "LED_BUILTINB": 4, + }, + "sensesiot_weizen": {}, + "sg-o_airMon": {}, + "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, + "tinypico": {}, + "ttgo-lora32-v1": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 2, + "MOSI": 27, + "SCK": 5, + "SS": 18, + }, + "ttgo-lora32-v2": { + "LED_BUILTIN": 22, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, + "ttgo-lora32-v21": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, + "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, + "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, + "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, + "ttgo-t7-v13-mini32": {"LED": 22}, + "ttgo-t7-v14-mini32": {"LED": 19}, + "turta_iot_node": {}, + "vintlabs-devkit-v1": { + "LED": 2, + "PWM0": 12, + "PWM1": 13, + "PWM2": 14, + "PWM3": 15, + "PWM4": 16, + "PWM5": 17, + "PWM6": 18, + "PWM7": 19, + }, + "wemos_d1_mini32": { + "D0": 26, + "D1": 22, + "D2": 21, + "D3": 17, + "D4": 16, + "D5": 18, + "D6": 19, + "D7": 23, + "D8": 5, + "LED": 2, + "RXD": 3, + "TXD": 1, + "_VBAT": 35, + }, + "wemosbat": {"LED": 16}, + "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, + "widora-air": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 25, + "MISO": 17, + "MOSI": 16, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "wifiduino32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + "SDA": 5, + "SCL": 16, + "A0": 27, + "A1": 14, + "A2": 12, + "A3": 35, + "A4": 13, + "A5": 4, + "D0": 3, + "D1": 1, + "D2": 17, + "D3": 15, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 23, + "D9": 22, + "D10": 21, + "D11": 19, + "D12": 18, + "D13": 2, + }, + "xinabox_cw02": {"LED": 27}, +} diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py new file mode 100644 index 0000000000..b82f03bf68 --- /dev/null +++ b/esphome/components/esp32/const.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg + +KEY_ESP32 = "esp32" +KEY_BOARD = "board" +KEY_VARIANT = "variant" +KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" + +VARIANT_ESP32 = "ESP32" +VARIANT_ESP32S2 = "ESP32S2" +VARIANT_ESP32S3 = "ESP32S3" +VARIANT_ESP32C3 = "ESP32C3" +VARIANT_ESP32H2 = "ESP32H2" +VARIANTS = [ + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, + VARIANT_ESP32H2, +] + +esp32_ns = cg.esphome_ns.namespace("esp32") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp new file mode 100644 index 0000000000..96047df535 --- /dev/null +++ b/esphome/components/esp32/core.cpp @@ -0,0 +1,89 @@ +#ifdef USE_ESP32 + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" +#include +#include +#include +#include + +#if ESP_IDF_VERSION_MAJOR >= 4 +#include +#endif + +void setup(); +void loop(); + +namespace esphome { + +void IRAM_ATTR HOT yield() { vPortYield(); } +uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); } +void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { + auto start = (uint64_t) esp_timer_get_time(); + while (((uint64_t) esp_timer_get_time()) - start < us) + ; +} +void arch_restart() { + esp_restart(); + // restart() doesn't always end execution + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} +void IRAM_ATTR HOT arch_feed_wdt() { +#ifdef USE_ARDUINO +#if CONFIG_ARDUINO_RUNNING_CORE == 0 +#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 + // ESP32 uses "Task Watchdog" which is hooked to the FreeRTOS idle task. + // To cause the Watchdog to be triggered we need to put the current task + // to sleep to get the idle task scheduled. + delay(1); +#endif +#endif +#endif // USE_ARDUINO + +#ifdef USE_ESP_IDF +#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 + delay(1); +#endif +#endif // USE_ESP_IDF +} + +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(); +#else + uint32_t ccount; + __asm__ __volatile__("esync; rsr %0,ccount" : "=a"(ccount)); + return ccount; +#endif +} +uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); } + +#ifdef USE_ESP_IDF +TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +void loop_task(void *pv_params) { + setup(); + while (true) { + loop(); + } +} + +extern "C" void app_main() { + esp32::setup_preferences(); + xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); +} +#endif // USE_ESP_IDF + +#ifdef USE_ARDUINO +extern "C" void init() { esp32::setup_preferences(); } +#endif // USE_ARDUINO + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py new file mode 100644 index 0000000000..93ab17db22 --- /dev/null +++ b/esphome/components/esp32/gpio.py @@ -0,0 +1,201 @@ +import logging + +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +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 KEY_BOARD, KEY_ESP32, esp32_ns + + +_LOGGER = logging.getLogger(__name__) + + +IDFInternalGPIOPin = esp32_ns.class_("IDFInternalGPIOPin", cg.InternalGPIOPin) +ArduinoInternalGPIOPin = esp32_ns.class_("ArduinoInternalGPIOPin", cg.InternalGPIOPin) + + +def _lookup_pin(value): + board = CORE.data[KEY_ESP32][KEY_BOARD] + board_pins = boards.ESP32_BOARD_PINS.get(board, {}) + + # Resolved aliased board pins (shorthand when two boards have the same pin configuration) + while isinstance(board_pins, str): + board_pins = boards.ESP32_BOARD_PINS[board_pins] + + if value in board_pins: + return board_pins[value] + if value in boards.ESP32_BASE_PINS: + return boards.ESP32_BASE_PINS[value] + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") + + +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): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return _lookup_pin(value) + + +_ESP_SDIO_PINS = { + 6: "Flash Clock", + 7: "Flash Data 0", + 8: "Flash Data 1", + 11: "Flash Command", +} + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > 39: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") + if value in _ESP_SDIO_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + ) + if 9 <= value <= 10: + _LOGGER.warning( + "Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", + value, + ) + if value in (20, 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 + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") + return value + + +def validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + 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_input: + # All ESP32 pins support input mode + pass + if is_output and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support output pin mode.", + [CONF_MODE, CONF_OUTPUT], + ) + 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_pullup and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] + ) + if is_pulldown and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] + ) + + if CORE.using_arduino: + # (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 on ESP32 for arduino frameworks", + [CONF_MODE], + ) + + return value + + +# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t +gpio_drive_cap_t = cg.global_ns.enum("gpio_drive_cap_t") +DRIVE_STRENGTHS = { + 5.0: gpio_drive_cap_t.GPIO_DRIVE_CAP_0, + 10.0: gpio_drive_cap_t.GPIO_DRIVE_CAP_1, + 20.0: gpio_drive_cap_t.GPIO_DRIVE_CAP_2, + 40.0: gpio_drive_cap_t.GPIO_DRIVE_CAP_3, +} +gpio_num_t = cg.global_ns.enum("gpio_num_t") + + +def _choose_pin_declaration(value): + if CORE.using_esp_idf: + return cv.declare_id(IDFInternalGPIOPin)(value) + if CORE.using_arduino: + return cv.declare_id(ArduinoInternalGPIOPin)(value) + raise NotImplementedError + + +CONF_DRIVE_STRENGTH = "drive_strength" +ESP32_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): _choose_pin_declaration, + 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.SplitDefault(CONF_DRIVE_STRENGTH, esp32_idf="20mA"): cv.All( + cv.only_with_esp_idf, + cv.float_with_unit("current", "mA", optional_unit=True), + cv.enum(DRIVE_STRENGTHS), + ), + }, + validate_supports, +) + + +@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) +async def esp32_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + if CORE.using_esp_idf: + cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) + else: + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + if CONF_DRIVE_STRENGTH in config: + cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/esp32/gpio_arduino.cpp b/esphome/components/esp32/gpio_arduino.cpp new file mode 100644 index 0000000000..c4bb21a0aa --- /dev/null +++ b/esphome/components/esp32/gpio_arduino.cpp @@ -0,0 +1,107 @@ +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "gpio_arduino.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace esp32 { + +static const char *const TAG = "esp32"; + +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 { + uint8_t arduino_mode = DISABLED; + 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_ ? ONHIGH : ONLOW; + break; + case gpio::INTERRUPT_HIGH_LEVEL: + arduino_mode = inverted_ ? ONLOW : ONHIGH; + break; + } + + attachInterruptArg(pin_, func, arg, arduino_mode); +} +void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { + uint8_t mode; + if (flags == gpio::FLAG_INPUT) { + mode = INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + mode = OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + mode = INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + mode = INPUT_PULLDOWN; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + mode = OUTPUT_OPEN_DRAIN; + } else { + return; + } + pinMode(pin_, mode); // NOLINT +} + +std::string ArduinoInternalGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool ArduinoInternalGPIOPin::digital_read() { + return bool(digitalRead(pin_)) != inverted_; // NOLINT +} +void ArduinoInternalGPIOPin::digital_write(bool value) { + digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT +} +void ArduinoInternalGPIOPin::detach_interrupt() const { + detachInterrupt(pin_); // NOLINT +} + +} // namespace esp32 + +using namespace esp32; + +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 ? 1 : 0); // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); +#ifdef CONFIG_IDF_TARGET_ESP32C3 + GPIO.status_w1tc.val = 1UL << arg->pin; +#else + if (arg->pin < 32) { + GPIO.status_w1tc = 1UL << arg->pin; + } else { + GPIO.status1_w1tc.intr_st = 1UL << (arg->pin - 32); + } +#endif +} + +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/esp32/gpio_arduino.h b/esphome/components/esp32/gpio_arduino.h new file mode 100644 index 0000000000..e88d39b1a8 --- /dev/null +++ b/esphome/components/esp32/gpio_arduino.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include "esphome/core/hal.h" + +namespace esphome { +namespace esp32 { + +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 esp32 +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/esp32/gpio_idf.cpp b/esphome/components/esp32/gpio_idf.cpp new file mode 100644 index 0000000000..d1853e1f8b --- /dev/null +++ b/esphome/components/esp32/gpio_idf.cpp @@ -0,0 +1,120 @@ +#ifdef USE_ESP32_FRAMEWORK_ESP_IDF + +#include "gpio_idf.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace esp32 { + +static const char *const TAG = "esp32"; + +bool IDFInternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +struct ISRPinArg { + gpio_num_t pin; + bool inverted; +}; + +ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void IDFInternalGPIOPin::setup() { + pin_mode(flags_); + gpio_set_drive_capability(pin_, drive_strength_); +} + +void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { + gpio_config_t conf{}; + conf.pin_bit_mask = 1ULL << static_cast(pin_); + conf.mode = flags_to_mode(flags); + conf.pull_up_en = flags & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + conf.pull_down_en = flags & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + conf.intr_type = GPIO_INTR_DISABLE; + gpio_config(&conf); +} + +bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } + +void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } + +gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { + flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); + if (flags == gpio::FLAG_NONE) { + return GPIO_MODE_DISABLE; + } else if (flags == gpio::FLAG_INPUT) { + return GPIO_MODE_INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return GPIO_MODE_OUTPUT; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return GPIO_MODE_OUTPUT_OD; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return GPIO_MODE_INPUT_OUTPUT_OD; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { + return GPIO_MODE_INPUT_OUTPUT; + } else { + // unsupported + return GPIO_MODE_DISABLE; + } +} + +void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + idf_type = inverted_ ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; + break; + case gpio::INTERRUPT_FALLING_EDGE: + idf_type = inverted_ ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; + break; + case gpio::INTERRUPT_ANY_EDGE: + idf_type = GPIO_INTR_ANYEDGE; + break; + case gpio::INTERRUPT_LOW_LEVEL: + idf_type = inverted_ ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; + break; + case gpio::INTERRUPT_HIGH_LEVEL: + idf_type = inverted_ ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; + break; + } + gpio_set_intr_type(pin_, idf_type); + gpio_intr_enable(pin_); + if (!isr_service_installed) { + auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); + if (res != ESP_OK) { + ESP_LOGE(TAG, "attach_interrupt(): call to gpio_install_isr_service() failed, error code: %d", res); + return; + } + isr_service_installed = true; + } + gpio_isr_handler_add(pin_, func, arg); +} + +std::string IDFInternalGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", static_cast(pin_)); + return buffer; +} + +} // namespace esp32 + +using namespace esp32; + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return bool(gpio_get_level(arg->pin)) != arg->inverted; +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + auto *arg = reinterpret_cast(arg_); + gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0); +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + // not supported +} + +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ESP_IDF diff --git a/esphome/components/esp32/gpio_idf.h b/esphome/components/esp32/gpio_idf.h new file mode 100644 index 0000000000..a99571cc46 --- /dev/null +++ b/esphome/components/esp32/gpio_idf.h @@ -0,0 +1,41 @@ +#pragma once + +#ifdef USE_ESP32_FRAMEWORK_ESP_IDF +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace esp32 { + +class IDFInternalGPIOPin : public InternalGPIOPin { + public: + void set_pin(gpio_num_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + 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 detach_interrupt() const override { gpio_intr_disable(pin_); } + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return (uint8_t) pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + static gpio_mode_t flags_to_mode(gpio::Flags flags); + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + + gpio_num_t pin_; + bool inverted_; + gpio_drive_cap_t drive_strength_; + gpio::Flags flags_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static bool isr_service_installed; +}; + +} // namespace esp32 +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ESP_IDF diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp new file mode 100644 index 0000000000..96b7e7809e --- /dev/null +++ b/esphome/components/esp32/preferences.cpp @@ -0,0 +1,153 @@ +#ifdef USE_ESP32 + +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include +#include +#include + +namespace esphome { +namespace esp32 { + +static const char *const TAG = "esp32.preferences"; + +struct NVSData { + std::string key; + std::vector data; +}; + +static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +class ESP32PreferenceBackend : public ESPPreferenceBackend { + public: + std::string key; + uint32_t nvs_handle; + bool save(const uint8_t *data, size_t len) override { + // try find in pending saves and update that + for (auto &obj : s_pending_save) { + if (obj.key == key) { + 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); + 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; + } + } + + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, key.c_str(), nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key.c_str(), esp_err_to_name(err)); + return false; + } + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len); + return false; + } + err = nvs_get_blob(nvs_handle, key.c_str(), data, &len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); + return false; + } + return true; + } +}; + +class ESP32Preferences : public ESPPreferences { + public: + uint32_t nvs_handle; + uint32_t current_offset = 0; + + void open() { + esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); + if (err == 0) + return; + + ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS...", esp_err_to_name(err)); + nvs_flash_deinit(); + nvs_flash_erase(); + nvs_flash_init(); + + err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); + if (err != 0) { + nvs_handle = 0; + } + } + 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 ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->nvs_handle = nvs_handle; + current_offset += length; + + uint32_t keyval = current_offset ^ type; + char keybuf[16]; + snprintf(keybuf, sizeof(keybuf), "%d", keyval); + pref->key = keybuf; // copied to std::string + + return ESPPreferenceObject(pref); + } + + bool sync() override { + if (s_pending_save.empty()) + return true; + + ESP_LOGD(TAG, "Saving preferences to flash..."); + // goal try write all pending saves even if one fails + bool any_failed = false; + + // 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_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), + esp_err_to_name(err)); + any_failed = true; + continue; + } + s_pending_save.erase(s_pending_save.begin() + i); + } + + // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes + esp_err_t err = nvs_commit(nvs_handle); + if (err != 0) { + ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); + return false; + } + + return !any_failed; + } +}; + +void setup_preferences() { + auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) + prefs->open(); + global_preferences = prefs; +} + +} // namespace esp32 + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32/preferences.h b/esphome/components/esp32/preferences.h new file mode 100644 index 0000000000..e44213e4cf --- /dev/null +++ b/esphome/components/esp32/preferences.h @@ -0,0 +1,12 @@ +#pragma once +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32 { + +void setup_preferences(); + +} // namespace esp32 +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index ccf1f6cafe..4b5c741ad9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,8 +1,10 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, ESP_PLATFORM_ESP32 +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] @@ -20,3 +22,6 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 756affaead..ecd591d169 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,17 +1,21 @@ -#include "ble.h" +#ifdef USE_ESP32 +#include "ble.h" #include "esphome/core/application.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 - #include #include #include #include +#include #include #include +#ifdef USE_ARDUINO +#include +#endif + namespace esphome { namespace esp32_ble { @@ -27,7 +31,7 @@ void ESP32BLE::setup() { return; } - this->advertising_ = new BLEAdvertising(); + this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) this->advertising_->set_scan_response(true); this->advertising_->set_min_preferred_interval(0x06); @@ -52,10 +56,37 @@ bool ESP32BLE::ble_setup_() { return false; } +#ifdef USE_ARDUINO if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); return false; } +#else + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + // start bt controller + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + 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 false; + } + 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 false; + } + } + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + ESP_LOGE(TAG, "esp bt controller enable failed"); + return false; + } + } +#endif esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); @@ -91,7 +122,16 @@ bool ESP32BLE::ble_setup_() { } } - err = esp_ble_gap_set_device_name(App.get_name().c_str()); + std::string name = App.get_name(); + if (name.length() > 20) { + if (App.is_name_add_mac_suffix_enabled()) { + name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address + } else { + name = name.substr(0, 20); + } + } + + err = esp_ble_gap_set_device_name(name.c_str()); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; @@ -114,25 +154,25 @@ void ESP32BLE::loop() { BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { - case ble_event->GATTS: + case BLEEvent::GATTS: this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, &ble_event->event_.gatts.gatts_param); break; - case ble_event->GAP: + case BLEEvent::GAP: this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); break; default: break; } - delete ble_event; + delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) ble_event = this->ble_events_.pop(); } } void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, param); + BLEEvent *new_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) global_ble->ble_events_.push(new_event); -} +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); @@ -144,9 +184,9 @@ void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, gatts_if, param); + BLEEvent *new_event = new BLEEvent(event, gatts_if, param); // NOLINT(cppcoreguidelines-owning-memory) global_ble->ble_events_.push(new_event); -} +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { @@ -165,7 +205,7 @@ float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); } -ESP32BLE *global_ble = nullptr; +ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 149f0008a4..0477dee070 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -11,7 +11,7 @@ #include "esphome/components/esp32_ble_server/ble_server.h" #endif -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include @@ -19,6 +19,7 @@ namespace esphome { namespace esp32_ble { +// NOLINTNEXTLINE(modernize-use-using) typedef struct { void *peer_device; bool connected; @@ -65,6 +66,7 @@ class ESP32BLE : public Component { BLEAdvertising *advertising_; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index f215fb48a3..31b1f4c383 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -1,12 +1,17 @@ #include "ble_advertising.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include "ble_uuid.h" +#include +#include +#include "esphome/core/log.h" namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + BLEAdvertising::BLEAdvertising() { this->advertising_data_.set_scan_rsp = false; this->advertising_data_.include_name = true; @@ -43,6 +48,7 @@ void BLEAdvertising::start() { this->advertising_data_.service_uuid_len = 0; } else { this->advertising_data_.service_uuid_len = 16 * num_services; + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) this->advertising_data_.p_service_uuid = new uint8_t[this->advertising_data_.service_uuid_len]; uint8_t *p = this->advertising_data_.p_service_uuid; for (int i = 0; i < num_services; i++) { diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d86089f333..01e2ba1295 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -2,7 +2,7 @@ #include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index cb0b99c62b..8556aa87df 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -1,10 +1,16 @@ #include "ble_uuid.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 + +#include +#include +#include "esphome/core/log.h" namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -31,28 +37,28 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ret.uuid_.len = ESP_UUID_LEN_16; ret.uuid_.uuid.uuid16 = 0; for (int i = 0; i < data.length();) { - uint8_t MSB = data.c_str()[i]; - uint8_t LSB = data.c_str()[i + 1]; + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; - if (MSB > '9') - MSB -= 7; - if (LSB > '9') - LSB -= 7; - ret.uuid_.uuid.uuid16 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (2 - i) * 4; + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; i += 2; } } else if (data.length() == 8) { ret.uuid_.len = ESP_UUID_LEN_32; ret.uuid_.uuid.uuid32 = 0; for (int i = 0; i < data.length();) { - uint8_t MSB = data.c_str()[i]; - uint8_t LSB = data.c_str()[i + 1]; + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; - if (MSB > '9') - MSB -= 7; - if (LSB > '9') - LSB -= 7; - ret.uuid_.uuid.uuid32 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (6 - i) * 4; + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; i += 2; } } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be @@ -67,14 +73,14 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { for (int i = 0; i < data.length();) { if (data.c_str()[i] == '-') i++; - uint8_t MSB = data.c_str()[i]; - uint8_t LSB = data.c_str()[i + 1]; + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; - if (MSB > '9') - MSB -= 7; - if (LSB > '9') - LSB -= 7; - ret.uuid_.uuid.uuid128[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F); + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); i += 2; } } else { diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 0e3c9b0d0a..f953f9fede 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -1,8 +1,9 @@ #pragma once #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 3c87a90f22..8d05eca058 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -1,19 +1,23 @@ #pragma once + +#ifdef USE_ESP32 + #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include #include - -#ifdef ARDUINO_ARCH_ESP32 +#include #include #include #include +#include +#include /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. @@ -24,33 +28,33 @@ namespace esp32_ble { template class Queue { public: - Queue() { m = xSemaphoreCreateMutex(); } + Queue() { m_ = xSemaphoreCreateMutex(); } void push(T *element) { if (element == nullptr) return; - if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { - q.push(element); - xSemaphoreGive(m); + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + q_.push(element); + xSemaphoreGive(m_); } } T *pop() { T *element = nullptr; - if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { - if (!q.empty()) { - element = q.front(); - q.pop(); + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + if (!q_.empty()) { + element = q_.front(); + q_.pop(); } - xSemaphoreGive(m); + xSemaphoreGive(m_); } return element; } protected: - std::queue q; - SemaphoreHandle_t m; + std::queue q_; + SemaphoreHandle_t m_; }; // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). @@ -101,11 +105,13 @@ class BLEEvent { }; union { + // NOLINTNEXTLINE(readability-identifier-naming) struct gap_event { esp_gap_ble_cb_event_t gap_event; esp_ble_gap_cb_param_t gap_param; } gap; + // NOLINTNEXTLINE(readability-identifier-naming) struct gattc_event { esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; @@ -113,6 +119,7 @@ class BLEEvent { uint8_t data[64]; } gattc; + // NOLINTNEXTLINE(readability-identifier-naming) struct gatts_event { esp_gatts_cb_event_t gatts_event; esp_gatt_if_t gatts_if; @@ -120,6 +127,7 @@ class BLEEvent { uint8_t data[64]; } gatts; } event_; + // NOLINTNEXTLINE(readability-identifier-naming) enum ble_event_t : uint8_t { GAP, GATTC, diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 3e00692d3a..d6cbb15dd2 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,8 +1,10 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, ESP_PLATFORM_ESP32 +from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] CONFLICTS_WITH = ["esp32_ble_tracker"] esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") @@ -24,10 +26,11 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): uuid = config[CONF_UUID].hex - uuid_arr = [ - cg.RawExpression("0x{}".format(uuid[i : i + 2])) for i in range(0, len(uuid), 2) - ] + 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) await cg.register_component(var, config) cg.add(var.set_major(config[CONF_MAJOR])) cg.add(var.set_minor(config[CONF_MINOR])) + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 1b06fd787e..f6bab8e6df 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -1,20 +1,27 @@ #include "esp32_ble_beacon.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include -#include +#include #include #include #include #include +#include +#include "esphome/core/hal.h" + +#ifdef USE_ARDUINO +#include +#endif namespace esphome { 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, @@ -28,7 +35,7 @@ static esp_ble_adv_params_t ble_adv_params = { #define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8)) -static esp_ble_ibeacon_head_t ibeacon_common_head = { +static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; void ESP32BLEBeacon::dump_config() { @@ -58,6 +65,7 @@ void ESP32BLEBeacon::ble_core_task(void *params) { delay(1000); // NOLINT } } + void ESP32BLEBeacon::ble_setup() { // Initialize non-volatile storage for the bluetooth controller esp_err_t err = nvs_flash_init(); @@ -66,10 +74,37 @@ void ESP32BLEBeacon::ble_setup() { 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); @@ -90,7 +125,7 @@ void ESP32BLEBeacon::ble_setup() { } 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_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t)); memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, global_esp32_ble_beacon->uuid_.data(), sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); @@ -131,7 +166,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap } } -ESP32BLEBeacon *global_esp32_ble_beacon = nullptr; +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 aba02830b3..80ad2041f2 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -2,13 +2,14 @@ #include "esphome/core/component.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include namespace esphome { namespace esp32_ble_beacon { +// NOLINTNEXTLINE(modernize-use-using) typedef struct { uint8_t flags[3]; uint8_t length; @@ -17,6 +18,7 @@ typedef struct { uint16_t beacon_type; } __attribute__((packed)) esp_ble_ibeacon_head_t; +// NOLINTNEXTLINE(modernize-use-using) typedef struct { uint8_t proximity_uuid[16]; uint16_t major; @@ -24,6 +26,7 @@ typedef struct { uint8_t measured_power; } __attribute__((packed)) esp_ble_ibeacon_vendor_t; +// NOLINTNEXTLINE(modernize-use-using) typedef struct { esp_ble_ibeacon_head_t ibeacon_head; esp_ble_ibeacon_vendor_t ibeacon_vendor; @@ -50,6 +53,7 @@ class ESP32BLEBeacon : public Component { uint16_t minor_{}; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLEBeacon *global_esp32_ble_beacon; } // namespace esp32_ble_beacon diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 0dcbcadc50..2fcc5c7743 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -1,12 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL, ESP_PLATFORM_ESP32 +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_tracker", "esp32_ble_beacon"] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" CONF_BLE_ID = "ble_id" @@ -37,3 +39,6 @@ async def to_code(config): cg.add_define("USE_ESP32_BLE_SERVER") cg.add(parent.set_server(var)) + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_2901.cpp b/esphome/components/esp32_ble_server/ble_2901.cpp index 962ba3ffbe..ee0808d2c4 100644 --- a/esphome/components/esp32_ble_server/ble_2901.cpp +++ b/esphome/components/esp32_ble_server/ble_2901.cpp @@ -1,7 +1,7 @@ #include "ble_2901.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { diff --git a/esphome/components/esp32_ble_server/ble_2901.h b/esphome/components/esp32_ble_server/ble_2901.h index 3bb23ae69d..60f53e55b2 100644 --- a/esphome/components/esp32_ble_server/ble_2901.h +++ b/esphome/components/esp32_ble_server/ble_2901.h @@ -2,7 +2,7 @@ #include "ble_descriptor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { diff --git a/esphome/components/esp32_ble_server/ble_2902.cpp b/esphome/components/esp32_ble_server/ble_2902.cpp index 0a87b239f9..2f34573c37 100644 --- a/esphome/components/esp32_ble_server/ble_2902.cpp +++ b/esphome/components/esp32_ble_server/ble_2902.cpp @@ -1,7 +1,9 @@ #include "ble_2902.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 + +#include namespace esphome { namespace esp32_ble_server { diff --git a/esphome/components/esp32_ble_server/ble_2902.h b/esphome/components/esp32_ble_server/ble_2902.h index 024eec755e..64605924ad 100644 --- a/esphome/components/esp32_ble_server/ble_2902.h +++ b/esphome/components/esp32_ble_server/ble_2902.h @@ -2,7 +2,7 @@ #include "ble_descriptor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 62775ab05f..fae8c13934 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -4,7 +4,7 @@ #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { @@ -27,7 +27,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(std::vector value) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = value; + this->value_ = std::move(value); xSemaphoreGive(this->set_value_lock_); } void BLECharacteristic::set_value(const std::string &value) { diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index bc5033f2ae..d7af3a934a 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -5,13 +5,15 @@ #include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include #include #include #include +#include +#include namespace esphome { namespace esp32_ble_server { @@ -22,7 +24,7 @@ class BLEService; class BLECharacteristic { public: - BLECharacteristic(const ESPBTUUID uuid, uint32_t properties); + BLECharacteristic(ESPBTUUID uuid, uint32_t properties); void set_value(const uint8_t *data, size_t length); void set_value(std::vector value); @@ -47,7 +49,7 @@ class BLECharacteristic { void do_create(BLEService *service); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void on_write(const std::function &)> &&func) { this->on_write_ = std::move(func); } + void on_write(const std::function &)> &&func) { this->on_write_ = func; } void add_descriptor(BLEDescriptor *descriptor); diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index a04343da3a..bfb6224335 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -1,10 +1,11 @@ #include "ble_descriptor.h" #include "ble_characteristic.h" #include "ble_service.h" - #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#include + +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { @@ -15,10 +16,10 @@ BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { this->uuid_ = uuid; this->value_.attr_len = 0; this->value_.attr_max_len = max_len; - this->value_.attr_value = (uint8_t *) malloc(max_len); + this->value_.attr_value = (uint8_t *) malloc(max_len); // NOLINT } -BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } +BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->characteristic_ = characteristic; diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 1a72cb2b54..4b8fb345c3 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -2,7 +2,7 @@ #include "esphome/components/esp32_ble/ble_uuid.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 2b4aea8ae0..00adc88060 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -5,7 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/version.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include @@ -79,11 +79,9 @@ bool BLEServer::create_device_characteristics_() { this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); model->set_value(this->model_.value()); } else { -#ifdef ARDUINO_BOARD BLECharacteristic *model = this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(ARDUINO_BOARD); -#endif + model->set_value(ESPHOME_BOARD); } BLECharacteristic *version = @@ -108,7 +106,7 @@ BLEService *BLEServer::create_service(const std::string &uuid, bool advertise) { } BLEService *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()); - BLEService *service = new BLEService(uuid, num_handles, inst_id); + BLEService *service = new BLEService(uuid, num_handles, inst_id); // NOLINT(cppcoreguidelines-owning-memory) this->services_.push_back(service); if (advertise) { esp32_ble::global_ble->get_advertising()->add_service_uuid(uuid); @@ -157,7 +155,7 @@ float BLEServer::get_setup_priority() const { return setup_priority::BLUETOOTH - void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } -BLEServer *global_ble_server = nullptr; +BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_ble_server } // namespace esphome diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 9d955dda79..9f7e8b8fc0 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -12,7 +12,7 @@ #include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include @@ -87,6 +87,7 @@ class BLEServer : public Component { } state_{INIT}; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern BLEServer *global_ble_server; } // namespace esp32_ble_server diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 563ee723af..19819b95cc 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -2,7 +2,7 @@ #include "ble_server.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_server { @@ -14,7 +14,7 @@ BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id) BLEService::~BLEService() { for (auto &chr : this->characteristics_) - delete chr; + delete chr; // NOLINT(cppcoreguidelines-owning-memory) } BLECharacteristic *BLEService::get_characteristic(ESPBTUUID uuid) { @@ -34,6 +34,7 @@ BLECharacteristic *BLEService::create_characteristic(const std::string &uuid, es return create_characteristic(ESPBTUUID::from_raw(uuid), properties); } BLECharacteristic *BLEService::create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) BLECharacteristic *characteristic = new BLECharacteristic(uuid, properties); this->characteristics_.push_back(characteristic); return characteristic; diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 9fdb95bde5..16cc897238 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -3,7 +3,7 @@ #include "ble_characteristic.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 5c70ddb27f..e3d52f345a 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -4,7 +4,6 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ID, - ESP_PLATFORM_ESP32, CONF_INTERVAL, CONF_DURATION, CONF_TRIGGER_ID, @@ -15,8 +14,10 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, ) +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] AUTO_LOAD = ["xiaomi_ble", "ruuvi_ble"] CONF_ESP32_BLE_ID = "esp32_ble_id" @@ -51,8 +52,7 @@ def validate_scan_parameters(config): if window > interval: raise cv.Invalid( - "Scan window ({}) needs to be smaller than scan interval ({})" - "".format(window, interval) + f"Scan window ({window}) needs to be smaller than scan interval ({interval})" ) if interval.total_milliseconds * 3 > duration.total_milliseconds: @@ -97,9 +97,7 @@ def bt_uuid(value): ) return value raise cv.Invalid( - "Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( - bt_uuid16_format, bt_uuid32_format, bt_uuid128_format - ) + f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" ) @@ -108,12 +106,20 @@ def as_hex(value): def as_hex_array(value): + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}") + + +def as_reversed_hex_array(value): value = value.replace("-", "") cpp_array = [ f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] ] return cg.RawExpression( - "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(reversed(cpp_array))) + f"(uint8_t*)(const uint8_t[16]){{{','.join(reversed(cpp_array))}}}" ) @@ -163,9 +169,6 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_MANUFACTURER_ID): bt_uuid, } ), - cv.Optional("scan_interval"): cv.invalid( - "This option has been removed in 1.14 (Reason: " "it never had an effect)" - ), } ).extend(cv.COMPONENT_SCHEMA) @@ -196,7 +199,7 @@ async def to_code(config): elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_SERVICE_UUID]) + uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID]) cg.add(trigger.set_service_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) @@ -208,12 +211,15 @@ async def to_code(config): elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID]) + uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID]) cg.add(trigger.set_manufacturer_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + async def register_ble_device(var, config): paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 9df2587ede..3505e9c26d 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -3,7 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_ble_tracker { diff --git a/esphome/components/esp32_ble_tracker/binary_sensor.py b/esphome/components/esp32_ble_tracker/binary_sensor.py deleted file mode 100644 index 3bea6d9900..0000000000 --- a/esphome/components/esp32_ble_tracker/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This platform has been renamed to ble_presence in 1.13") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 2a3403f88d..65749f5124 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,18 +1,24 @@ +#ifdef USE_ESP32 + #include "esp32_ble_tracker.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" - -#ifdef ARDUINO_ARCH_ESP32 +#include "esphome/core/hal.h" #include #include #include #include +#include #include #include #include +#ifdef USE_ARDUINO +#include +#endif + // bt_trace.h #undef TAG @@ -21,7 +27,7 @@ namespace esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; -ESP32BLETracker *global_esp32_ble_tracker = nullptr; +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; @@ -44,29 +50,29 @@ void ESP32BLETracker::setup() { return; } - global_esp32_ble_tracker->start_scan(true); + global_esp32_ble_tracker->start_scan_(true); } void ESP32BLETracker::loop() { BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { if (ble_event->type_) - this->real_gattc_event_handler(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); + this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); else - this->real_gap_event_handler(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); - delete ble_event; + this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) ble_event = this->ble_events_.pop(); } bool connecting = false; for (auto *client : this->clients_) { - if (client->state() == ClientState::Connecting || client->state() == ClientState::Discovered) + if (client->state() == ClientState::CONNECTING || client->state() == ClientState::DISCOVERED) connecting = true; } if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { xSemaphoreGive(this->scan_end_lock_); - global_esp32_ble_tracker->start_scan(false); + global_esp32_ble_tracker->start_scan_(false); } if (xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { @@ -88,7 +94,7 @@ void ESP32BLETracker::loop() { for (auto *client : this->clients_) if (client->parse_device(device)) { found = true; - if (client->state() == ClientState::Discovered) { + if (client->state() == ClientState::DISCOVERED) { esp_ble_gap_stop_scanning(); if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { xSemaphoreGive(this->scan_end_lock_); @@ -126,13 +132,39 @@ bool ESP32BLETracker::ble_setup() { return false; } - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - - // Initialize the bluetooth controller with the default configuration +#ifdef USE_ARDUINO if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); return false; } +#else + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + // start bt controller + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + 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 false; + } + 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 false; + } + } + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + ESP_LOGE(TAG, "esp bt controller enable failed"); + return false; + } + } +#endif + + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); err = esp_bluedroid_init(); if (err != ESP_OK) { @@ -171,7 +203,7 @@ bool ESP32BLETracker::ble_setup() { return true; } -void ESP32BLETracker::start_scan(bool first) { +void ESP32BLETracker::start_scan_(bool first) { if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { ESP_LOGW(TAG, "Cannot start scan!"); return; @@ -204,42 +236,42 @@ void ESP32BLETracker::register_client(ESPBTClient *client) { } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *gap_event = new BLEEvent(event, param); + BLEEvent *gap_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) global_esp32_ble_tracker->ble_events_.push(gap_event); -} +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) -void ESP32BLETracker::real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { +void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: - global_esp32_ble_tracker->gap_scan_result(param->scan_rst); + global_esp32_ble_tracker->gap_scan_result_(param->scan_rst); break; case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_set_param_complete(param->scan_param_cmpl); + global_esp32_ble_tracker->gap_scan_set_param_complete_(param->scan_param_cmpl); break; case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_start_complete(param->scan_start_cmpl); + global_esp32_ble_tracker->gap_scan_start_complete_(param->scan_start_cmpl); break; case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_stop_complete(param->scan_stop_cmpl); + global_esp32_ble_tracker->gap_scan_stop_complete_(param->scan_stop_cmpl); break; default: break; } } -void ESP32BLETracker::gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { +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; } -void ESP32BLETracker::gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { +void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { this->scan_start_failed_ = param.status; } -void ESP32BLETracker::gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { +void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { xSemaphoreGive(this->scan_end_lock_); } -void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { +void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { if (this->scan_result_index_ < 16) { @@ -254,12 +286,12 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); + BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) global_esp32_ble_tracker->ble_events_.push(gattc_event); -} +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) -void ESP32BLETracker::real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { +void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { for (auto *client : global_esp32_ble_tracker->clients_) { client->gattc_event_handler(event, gattc_if, param); } @@ -285,6 +317,63 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; return ret; } +ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { + ESPBTUUID ret; + if (data.length() == 4) { + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = 0; + for (int i = 0; i < data.length();) { + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; + i += 2; + } + } else if (data.length() == 8) { + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = 0; + for (int i = 0; i < data.length();) { + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; + i += 2; + } + } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be + // investigated (lack of time) + ret.uuid_.len = ESP_UUID_LEN_128; + memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); + } else if (data.length() == 36) { + // If the length of the string is 36 bytes then we will assume it is a long hex string in + // UUID format. + ret.uuid_.len = ESP_UUID_LEN_128; + int n = 0; + for (int i = 0; i < data.length();) { + if (data.c_str()[i] == '-') + i++; + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); + i += 2; + } + } else { + ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); + } + return ret; +} ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { ESPBTUUID ret; ret.uuid_.len = uuid.len; @@ -355,8 +444,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { } return false; } -esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } -std::string ESPBTUUID::to_string() { +esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } +std::string ESPBTUUID::to_string() const { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: @@ -372,7 +461,7 @@ std::string ESPBTUUID::to_string() { for (int8_t i = 15; i >= 0; i--) { sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); bpos += 2; - if (i == 3 || i == 5 || i == 7 || i == 9) + if (i == 6 || i == 8 || i == 10 || i == 12) sprintf(bpos++, "-"); } sbuf[47] = '\0'; @@ -434,6 +523,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e } for (auto &data : this->manufacturer_datas_) { ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(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()); + } } for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 6f0c28a73c..1308119df5 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "queue.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include @@ -25,6 +25,8 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + static ESPBTUUID from_raw(const std::string &data); + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); ESPBTUUID as_128bit() const; @@ -34,9 +36,9 @@ class ESPBTUUID { bool operator==(const ESPBTUUID &uuid) const; bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - esp_bt_uuid_t get_uuid(); + esp_bt_uuid_t get_uuid() const; - std::string to_string(); + std::string to_string() const; protected: esp_bt_uuid_t uuid_; @@ -85,12 +87,6 @@ class ESPBTDevice { int get_rssi() const { return rssi_; } const std::string &get_name() const { return this->name_; } - ESPDEPRECATED("Use get_tx_powers() instead") - optional get_tx_power() const { - if (this->tx_powers_.empty()) - return {}; - return this->tx_powers_[0]; - } const std::vector &get_tx_powers() const { return tx_powers_; } const optional &get_appearance() const { return appearance_; } @@ -141,15 +137,15 @@ class ESPBTDeviceListener { enum class ClientState { // Connection is idle, no device detected. - Idle, + IDLE, // Device advertisement found. - Discovered, + DISCOVERED, // Connection in progress. - Connecting, + CONNECTING, // Initial connection established. - Connected, + CONNECTED, // The client and sub-clients have completed setup. - Established, + ESTABLISHED, }; class ESPBTClient : public ESPBTDeviceListener { @@ -191,23 +187,23 @@ class ESP32BLETracker : public Component { /// The FreeRTOS task managing the bluetooth interface. static bool ble_setup(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. - void start_scan(bool first); + void start_scan_(bool first); /// Callback that will handle all GAP events and redistribute them to other callbacks. static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. - void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. - void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); + void gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. - void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); + void gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. - void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); + void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); int app_id_; /// Callback that will handle all GATTC events and redistribute them to other callbacks. static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -231,6 +227,7 @@ class ESP32BLETracker : public Component { Queue ble_events_; }; +// NOLINTNEXTLINE extern ESP32BLETracker *global_esp32_ble_tracker; } // namespace esp32_ble_tracker diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h index 17adb98034..f09b2ca8d7 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -1,18 +1,21 @@ #pragma once + +#ifdef USE_ESP32 #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include #include - -#ifdef ARDUINO_ARCH_ESP32 +#include #include #include +#include +#include /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. @@ -23,33 +26,33 @@ namespace esp32_ble_tracker { template class Queue { public: - Queue() { m = xSemaphoreCreateMutex(); } + Queue() { m_ = xSemaphoreCreateMutex(); } void push(T *element) { if (element == nullptr) return; - if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { - q.push(element); - xSemaphoreGive(m); + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + q_.push(element); + xSemaphoreGive(m_); } } T *pop() { T *element = nullptr; - if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { - if (!q.empty()) { - element = q.front(); - q.pop(); + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + if (!q_.empty()) { + element = q_.front(); + q_.pop(); } - xSemaphoreGive(m); + xSemaphoreGive(m_); } return element; } protected: - std::queue q; - SemaphoreHandle_t m; + std::queue q_; + SemaphoreHandle_t m_; }; // Received GAP and GATTC events are only queued, and get processed in the main loop(). @@ -84,12 +87,12 @@ class BLEEvent { }; union { - struct gap_event { + struct gap_event { // NOLINT(readability-identifier-naming) esp_gap_ble_cb_event_t gap_event; esp_ble_gap_cb_param_t gap_param; } gap; - struct gattc_event { + struct gattc_event { // NOLINT(readability-identifier-naming) esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; esp_ble_gattc_cb_param_t gattc_param; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index abbb4b1b7e..7f3aebe238 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,25 +2,26 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, - ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS, CONF_CONTRAST, ) +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -DEPENDENCIES = ["api"] +DEPENDENCIES = ["esp32", "api"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") -ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.Nameable) +ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") FRAME_SIZES = { "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, @@ -66,13 +67,16 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32Camera), cv.Required(CONF_NAME): cv.string, - cv.Required(CONF_DATA_PINS): cv.All([pins.input_pin], cv.Length(min=8, max=8)), - cv.Required(CONF_VSYNC_PIN): pins.input_pin, - cv.Required(CONF_HREF_PIN): pins.input_pin, - cv.Required(CONF_PIXEL_CLOCK_PIN): pins.input_pin, + cv.Optional(CONF_DISABLED_BY_DEFAULT, default=False): cv.boolean, + cv.Required(CONF_DATA_PINS): cv.All( + [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) + ), + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( { - cv.Required(CONF_PIN): pins.output_pin, + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( cv.frequency, cv.one_of(20e6, 10e6) ), @@ -80,12 +84,12 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Required(CONF_I2C_PINS): cv.Schema( { - cv.Required(CONF_SDA): pins.output_pin, - cv.Required(CONF_SCL): pins.output_pin, + cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, + cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, } ), - cv.Optional(CONF_RESET_PIN): pins.output_pin, - cv.Optional(CONF_POWER_DOWN_PIN): pins.output_pin, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( cv.framerate, cv.Range(min=0, min_included=False, max=60) ), @@ -124,6 +128,7 @@ SETTERS = { async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) await cg.register_component(var, config) for key, setter in SETTERS.items(): @@ -143,3 +148,8 @@ async def to_code(config): cg.add_define("USE_ESP32_CAMERA") cg.add_build_flag("-DBOARD_HAS_PSRAM") + + 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_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 500ddd67c9..babfda4113 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -1,7 +1,10 @@ +#ifdef USE_ESP32 + #include "esp32_camera.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" -#ifdef ARDUINO_ARCH_ESP32 +#include namespace esphome { namespace esp32_camera { @@ -42,7 +45,9 @@ void ESP32Camera::dump_config() { auto conf = this->config_; ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); +#ifdef USE_ARDUINO ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); +#endif // USE_ARDUINO ESP_LOGCONFIG(TAG, " Data Pins: D0:%d D1:%d D2:%d D3:%d D4:%d D5:%d D6:%d D7:%d", conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7); ESP_LOGCONFIG(TAG, " VSYNC Pin: %d", conf.pin_vsync); @@ -167,7 +172,7 @@ void ESP32Camera::framebuffer_task(void *pv) { esp_camera_fb_return(framebuffer); } } -ESP32Camera::ESP32Camera(const std::string &name) : Nameable(name) { +ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(name) { this->config_.pin_pwdn = -1; this->config_.pin_reset = -1; this->config_.pin_xclk = -1; @@ -275,10 +280,10 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { } void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; } -ESP32Camera *global_esp32_camera; +ESP32Camera *global_esp32_camera; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void CameraImageReader::set_image(std::shared_ptr image) { - this->image_ = image; + this->image_ = std::move(image); this->offset_ = 0; } size_t CameraImageReader::available() const { diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 03272d3b32..d0445607a4 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -1,10 +1,13 @@ #pragma once -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include +#include +#include namespace esphome { namespace esp32_camera { @@ -48,7 +51,7 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_1600X1200, // UXGA }; -class ESP32Camera : public Component, public Nameable { +class ESP32Camera : public Component, public EntityBase { public: ESP32Camera(const std::string &name); void set_data_pins(std::array pins); @@ -104,6 +107,7 @@ class ESP32Camera : public Component, public Nameable { uint32_t last_update_{0}; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; } // namespace esp32_camera diff --git a/esphome/components/esp32_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp index 20a047f0ba..7f37e2ce47 100644 --- a/esphome/components/esp32_dac/esp32_dac.cpp +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -2,9 +2,14 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 +#ifdef USE_ARDUINO #include +#endif +#ifdef USE_ESP_IDF +#include +#endif namespace esphome { namespace esp32_dac { @@ -15,6 +20,11 @@ void ESP32DAC::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 DAC Output..."); this->pin_->setup(); this->turn_off(); + +#ifdef USE_ESP_IDF + auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2; + dac_output_enable(channel); +#endif } void ESP32DAC::dump_config() { @@ -28,7 +38,14 @@ void ESP32DAC::write_state(float state) { state = 1.0f - state; state = state * 255; + +#ifdef USE_ESP_IDF + auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2; + dac_output_voltage(channel, (uint8_t) state); +#endif +#ifdef USE_ARDUINO dacWrite(this->pin_->get_pin(), state); +#endif } } // namespace esp32_dac diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h index 648efcfe4f..0fb1ddebf0 100644 --- a/esphome/components/esp32_dac/esp32_dac.h +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -1,18 +1,18 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_dac { class ESP32DAC : public output::FloatOutput, public Component { public: - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } /// Initialize pin void setup() override; @@ -23,7 +23,7 @@ class ESP32DAC : public output::FloatOutput, public Component { protected: void write_state(float state) override; - GPIOPin *pin_; + InternalGPIOPin *pin_; }; } // namespace esp32_dac diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index 8534a1bae1..f119198618 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -2,9 +2,9 @@ from esphome import pins from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN, ESP_PLATFORM_ESP32 +from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] def valid_dac_pin(value): diff --git a/esphome/components/esp32_hall/esp32_hall.cpp b/esphome/components/esp32_hall/esp32_hall.cpp index 4bbf65e048..762497aedc 100644 --- a/esphome/components/esp32_hall/esp32_hall.cpp +++ b/esphome/components/esp32_hall/esp32_hall.cpp @@ -1,8 +1,8 @@ +#ifdef USE_ESP32 #include "esp32_hall.h" #include "esphome/core/log.h" -#include "esphome/core/esphal.h" - -#ifdef ARDUINO_ARCH_ESP32 +#include "esphome/core/hal.h" +#include namespace esphome { namespace esp32_hall { @@ -10,7 +10,9 @@ namespace esp32_hall { static const char *const TAG = "esp32_hall"; void ESP32HallSensor::update() { - float value = (hallRead() / 4095.0f) * 10000.0f; + adc1_config_width(ADC_WIDTH_BIT_12); + int value_int = hall_sensor_read(); + float value = (value_int / 4095.0f) * 10000.0f; ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value); this->publish_state(value); } diff --git a/esphome/components/esp32_hall/esp32_hall.h b/esphome/components/esp32_hall/esp32_hall.h index 040280fff3..8db50c4667 100644 --- a/esphome/components/esp32_hall/esp32_hall.h +++ b/esphome/components/esp32_hall/esp32_hall.h @@ -3,7 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_hall { diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index b800b3436a..a752da2c97 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -3,14 +3,12 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, - ESP_PLATFORM_ESP32, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, ICON_MAGNET, ) -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") ESP32HallSensor = esp32_hall_ns.class_( @@ -19,7 +17,10 @@ ESP32HallSensor = esp32_hall_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index c9890455d6..e8429790ea 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,14 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, output, esp32_ble_server -from esphome.const import CONF_BLE_SERVER_ID, CONF_ID, ESP_PLATFORM_ESP32 +from esphome.const import CONF_BLE_SERVER_ID, CONF_ID AUTO_LOAD = ["binary_sensor", "output", "improv", "esp32_ble_server"] CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] -DEPENDENCIES = ["wifi"] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["wifi", "esp32"] CONF_AUTHORIZED_DURATION = "authorized_duration" CONF_AUTHORIZER = "authorizer" diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 65d253078c..faa9ab7df6 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -5,7 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_improv { @@ -32,7 +32,7 @@ void ESP32ImprovComponent::setup_characteristics() { this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_->on_write([this](const std::vector &data) { - if (data.size() > 0) { + if (!data.empty()) { this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); } }); @@ -56,7 +56,7 @@ void ESP32ImprovComponent::setup_characteristics() { } void ESP32ImprovComponent::loop() { - if (this->incoming_data_.size() > 0) + if (!this->incoming_data_.empty()) this->process_incoming_data_(); uint32_t now = millis(); @@ -126,7 +126,7 @@ void ESP32ImprovComponent::loop() { std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); - this->send_response(data); + this->send_response_(data); this->set_timeout("end-service", 1000, [this] { this->service_->stop(); this->set_state_(improv::STATE_STOPPED); @@ -162,7 +162,7 @@ bool ESP32ImprovComponent::check_identify_() { void ESP32ImprovComponent::set_state_(improv::State state) { ESP_LOGV(TAG, "Setting state: %d", state); this->state_ = state; - if (this->status_->get_value().size() == 0 || this->status_->get_value()[0] != state) { + if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { uint8_t data[1]{state}; this->status_->set_value(data, 1); if (state != improv::STATE_STOPPED) @@ -173,7 +173,7 @@ void ESP32ImprovComponent::set_state_(improv::State state) { void ESP32ImprovComponent::set_error_(improv::Error error) { if (error != improv::ERROR_NONE) ESP_LOGE(TAG, "Error: %d", error); - if (this->error_->get_value().size() == 0 || this->error_->get_value()[0] != error) { + if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { uint8_t data[1]{error}; this->error_->set_value(data, 1); if (this->state_ != improv::STATE_STOPPED) @@ -181,7 +181,7 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { } } -void ESP32ImprovComponent::send_response(std::vector &response) { +void ESP32ImprovComponent::send_response_(std::vector &response) { this->rpc_response_->set_value(response); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); @@ -274,7 +274,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); }; -ESP32ImprovComponent *global_improv_component = nullptr; +ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_improv } // namespace esphome diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 262124f983..53cda5f399 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -10,7 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace esp32_improv { @@ -63,12 +63,13 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void set_state_(improv::State state); void set_error_(improv::Error error); - void send_response(std::vector &response); + void send_response_(std::vector &response); void process_incoming_data_(); void on_wifi_connect_timeout_(); bool check_identify_(); }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32ImprovComponent *global_improv_component; } // namespace esp32_improv diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 1564476ecf..cdf6aa3abd 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -9,12 +9,11 @@ from esphome.const import ( CONF_SETUP_MODE, CONF_SLEEP_DURATION, CONF_VOLTAGE_ATTENUATION, - ESP_PLATFORM_ESP32, ) from esphome.core import TimePeriod AUTO_LOAD = ["binary_sensor"] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 300de23f08..bd3e06545d 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -2,19 +2,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import ( - CONF_NAME, CONF_PIN, CONF_THRESHOLD, - ESP_PLATFORM_ESP32, CONF_ID, ) -from esphome.pins import validate_gpio_pin +from esphome.components.esp32 import gpio from . import esp32_touch_ns, ESP32TouchComponent -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -DEPENDENCIES = ["esp32_touch"] +DEPENDENCIES = ["esp32_touch", "esp32"] CONF_ESP32_TOUCH_ID = "esp32_touch_id" +CONF_WAKEUP_THRESHOLD = "wakeup_threshold" TOUCH_PADS = { 4: cg.global_ns.TOUCH_PAD_NUM0, @@ -31,7 +29,7 @@ TOUCH_PADS = { def validate_touch_pad(value): - value = validate_gpio_pin(value) + value = gpio.validate_gpio_pin(value) if value not in TOUCH_PADS: raise cv.Invalid(f"Pin {value} does not support touch pads.") return value @@ -47,6 +45,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( 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, } ) @@ -55,9 +54,9 @@ async def to_code(config): hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID]) var = cg.new_Pvariable( config[CONF_ID], - config[CONF_NAME], TOUCH_PADS[config[CONF_PIN]], config[CONF_THRESHOLD], + config[CONF_WAKEUP_THRESHOLD], ) await binary_sensor.register_binary_sensor(var, config) cg.add(hub.register_touch_pad(var)) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index a990e632af..cb72820900 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -1,7 +1,8 @@ +#ifdef USE_ESP32 + #include "esp32_touch.h" #include "esphome/core/log.h" - -#ifdef ARDUINO_ARCH_ESP32 +#include "esphome/core/hal.h" namespace esphome { namespace esp32_touch { @@ -133,15 +134,33 @@ void ESP32TouchComponent::loop() { } void ESP32TouchComponent::on_shutdown() { + bool is_wakeup_source = false; + if (this->iir_filter_enabled_()) { touch_pad_filter_stop(); touch_pad_filter_delete(); } - touch_pad_deinit(); + + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + if (!is_wakeup_source) { + is_wakeup_source = true; + // Touch sensor FSM mode must be 'TOUCH_FSM_MODE_TIMER' to use it to wake-up. + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + } + + // No filter available when using as wake-up source. + touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); + } + } + + if (!is_wakeup_source) { + touch_pad_deinit(); + } } -ESP32TouchBinarySensor::ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold) - : BinarySensor(name), touch_pad_(touch_pad), threshold_(threshold) {} +ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_t wakeup_threshold) + : BinarySensor(), touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} } // namespace esp32_touch } // namespace esphome diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 45d459a2ff..d49e4703a7 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -1,9 +1,16 @@ #pragma once +#ifdef USE_ESP32 + #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include -#ifdef ARDUINO_ARCH_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 4 +#include +#else +#include +#endif namespace esphome { namespace esp32_touch { @@ -57,12 +64,13 @@ class ESP32TouchComponent : public Component { /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_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_; } protected: friend ESP32TouchComponent; @@ -70,6 +78,7 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_; uint16_t threshold_; uint16_t value_; + const uint16_t wakeup_threshold_; }; } // namespace esp32_touch diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py new file mode 100644 index 0000000000..93a461ba1f --- /dev/null +++ b/esphome/components/esp8266/__init__.py @@ -0,0 +1,217 @@ +import logging + +from esphome.const import ( + CONF_BOARD, + CONF_BOARD_FLASH_MODE, + CONF_FRAMEWORK, + CONF_VERSION, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE, coroutine_with_priority +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, esp8266_ns +from .boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS + +# force import gpio to register pin schema +from .gpio import esp8266_pin_to_code # noqa + + +CODEOWNERS = ["@esphome/core"] +_LOGGER = logging.getLogger(__name__) +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_FRAMEWORK] = "arduino" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + config[CONF_FRAMEWORK][CONF_VERSION_HINT] + ) + CORE.data[KEY_ESP8266][KEY_BOARD] = config[CONF_BOARD] + return config + + +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 + # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 + if ver <= cv.Version(2, 4, 1): + return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + if ver <= cv.Version(2, 6, 2): + return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + +# NOTE: Keep this in mind when updating the recommended version: +# * New framework historically have had some regressions, especially for WiFi. +# The new version needs to be thoroughly validated before changing the +# recommended version as otherwise a bunch of devices could be bricked +# * For all constants below, update platformio.ini (in this repo) +# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository + +# 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(2, 7, 4) +# 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, 2) +# for arduino 3 framework versions +ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 0, 2) + + +def _arduino_check_versions(value): + value = value.copy() + lookups = { + "dev": ("https://github.com/esp8266/Arduino.git", cv.Version(3, 0, 2)), + "latest": ("", cv.Version(3, 0, 2)), + "recommended": ( + _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), + RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, + ), + } + ver_value = value[CONF_VERSION] + default_ver_hint = None + if ver_value.lower() in lookups: + default_ver_hint = str(lookups[ver_value.lower()][1]) + ver_value = lookups[ver_value.lower()][0] + else: + with cv.suppress_invalid(): + ver = cv.Version.parse(cv.version_number(value)) + if ver <= cv.Version(2, 4, 1): + ver_value = f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + elif ver <= cv.Version(2, 6, 2): + ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + else: + ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + default_ver_hint = str(ver) + + value[CONF_VERSION] = ver_value + + if CONF_VERSION_HINT not in value and default_ver_hint is None: + raise cv.Invalid("Needs a version hint to understand the framework version") + + ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) + value[CONF_VERSION_HINT] = ver_hint_s + plat_ver = value.get(CONF_PLATFORM_VERSION) + + if plat_ver is None: + ver_hint = cv.Version.parse(ver_hint_s) + if ver_hint >= cv.Version(3, 0, 0): + plat_ver = ARDUINO_3_PLATFORM_VERSION + elif ver_hint >= cv.Version(2, 5, 0): + plat_ver = ARDUINO_2_PLATFORM_VERSION + else: + plat_ver = cv.Version(1, 8, 0) + value[CONF_PLATFORM_VERSION] = str(plat_ver) + + if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected arduino framework version is not the recommended one" + ) + _LOGGER.warning( + "If there are connectivity or build issues please remove the manual version" + ) + + return value + + +CONF_VERSION_HINT = "version_hint" +CONF_PLATFORM_VERSION = "platform_version" +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + } + ), + _arduino_check_versions, +) + + +BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"] +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, + cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, + cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( + *BUILD_FLASH_MODES, lower=True + ), + } + ), + set_core_data, +) + + +@coroutine_with_priority(1000) +async def to_code(config): + cg.add(esp8266_ns.setup_preferences()) + + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_ESP8266") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + + conf = config[CONF_FRAMEWORK] + cg.add_platformio_option("framework", "arduino") + cg.add_build_flag("-DUSE_ARDUINO") + cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") + cg.add_platformio_option( + "platform_packages", + [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_VERSION]}"], + ) + cg.add_platformio_option( + "platform", f"platformio/espressif8266 @ {conf[CONF_PLATFORM_VERSION]}" + ) + + # Default for platformio is LWIP2_LOW_MEMORY with: + # - MSS=536 + # - LWIP_FEATURES enabled + # - this only adds some optional features like IP incoming packet reassembly and NAPT + # see also: + # https://github.com/esp8266/Arduino/blob/master/tools/sdk/lwip2/include/lwipopts.h + + # Instead we use LWIP2_HIGHER_BANDWIDTH_LOW_FLASH with: + # - MSS=1460 + # - LWIP_FEATURES disabled (because we don't need them) + # Other projects like Tasmota & ESPEasy also use this + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH") + + if config[CONF_RESTORE_FROM_FLASH]: + cg.add_define("USE_ESP8266_PREFERENCES_FLASH") + + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when + # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make + # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of + # a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3, + # which always aborts if exceptions are disabled. + # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` + cg.add_build_flag("-DNEW_OOM_ABORT") + + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) + + if config[CONF_BOARD] in ESP8266_FLASH_SIZES: + flash_size = ESP8266_FLASH_SIZES[config[CONF_BOARD]] + ld_scripts = ESP8266_LD_SCRIPTS[flash_size] + ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + + if ver <= cv.Version(2, 3, 0): + # No ld script support + ld_script = None + if ver <= cv.Version(2, 4, 2): + # Old ld script path + ld_script = ld_scripts[0] + else: + ld_script = ld_scripts[1] + + if ld_script is not None: + cg.add_platformio_option("board_build.ldscript", ld_script) diff --git a/esphome/components/esp8266/boards.py b/esphome/components/esp8266/boards.py new file mode 100644 index 0000000000..c49aae4ffa --- /dev/null +++ b/esphome/components/esp8266/boards.py @@ -0,0 +1,266 @@ +FLASH_SIZE_1_MB = 2 ** 20 +FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 +FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB +FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB +FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB + +ESP8266_FLASH_SIZES = { + "d1": FLASH_SIZE_4_MB, + "d1_mini": FLASH_SIZE_4_MB, + "d1_mini_lite": FLASH_SIZE_1_MB, + "d1_mini_pro": FLASH_SIZE_16_MB, + "esp01": FLASH_SIZE_512_KB, + "esp01_1m": FLASH_SIZE_1_MB, + "esp07": FLASH_SIZE_4_MB, + "esp12e": FLASH_SIZE_4_MB, + "esp210": FLASH_SIZE_4_MB, + "esp8285": FLASH_SIZE_1_MB, + "esp_wroom_02": FLASH_SIZE_2_MB, + "espduino": FLASH_SIZE_4_MB, + "espectro": FLASH_SIZE_4_MB, + "espino": FLASH_SIZE_4_MB, + "espinotee": FLASH_SIZE_4_MB, + "espmxdevkit": FLASH_SIZE_1_MB, + "espresso_lite_v1": FLASH_SIZE_4_MB, + "espresso_lite_v2": FLASH_SIZE_4_MB, + "gen4iod": FLASH_SIZE_512_KB, + "heltec_wifi_kit_8": FLASH_SIZE_4_MB, + "huzzah": FLASH_SIZE_4_MB, + "inventone": FLASH_SIZE_4_MB, + "modwifi": FLASH_SIZE_2_MB, + "nodemcu": FLASH_SIZE_4_MB, + "nodemcuv2": FLASH_SIZE_4_MB, + "oak": FLASH_SIZE_4_MB, + "phoenix_v1": FLASH_SIZE_4_MB, + "phoenix_v2": FLASH_SIZE_4_MB, + "sonoff_basic": FLASH_SIZE_1_MB, + "sonoff_s20": FLASH_SIZE_1_MB, + "sonoff_sv": FLASH_SIZE_1_MB, + "sonoff_th": FLASH_SIZE_1_MB, + "sparkfunBlynk": FLASH_SIZE_4_MB, + "thing": FLASH_SIZE_512_KB, + "thingdev": FLASH_SIZE_512_KB, + "wifi_slot": FLASH_SIZE_1_MB, + "wifiduino": FLASH_SIZE_4_MB, + "wifinfo": FLASH_SIZE_1_MB, + "wio_link": FLASH_SIZE_4_MB, + "wio_node": FLASH_SIZE_4_MB, + "xinabox_cw01": FLASH_SIZE_4_MB, +} + +ESP8266_LD_SCRIPTS = { + FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), + FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), + FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), + FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), + FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), +} + +ESP8266_BASE_PINS = { + "A0": 17, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SDA": 4, + "SCL": 5, + "RX": 3, + "TX": 1, +} + +ESP8266_BOARD_PINS = { + "d1": { + "D0": 3, + "D1": 1, + "D2": 16, + "D3": 5, + "D4": 4, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 0, + "D9": 2, + "D10": 15, + "D11": 13, + "D12": 14, + "D13": 14, + "D14": 4, + "D15": 5, + "LED": 2, + }, + "d1_mini": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "LED": 2, + }, + "d1_mini_lite": "d1_mini", + "d1_mini_pro": "d1_mini", + "esp01": {}, + "esp01_1m": {}, + "esp07": {}, + "esp12e": {}, + "esp210": {}, + "esp8285": {}, + "esp_wroom_02": {}, + "espduino": {"LED": 16}, + "espectro": {"LED": 15, "BUTTON": 2}, + "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, + "espinotee": {"LED": 16}, + "espmxdevkit": {}, + "espresso_lite_v1": {"LED": 16}, + "espresso_lite_v2": {"LED": 2}, + "gen4iod": {}, + "heltec_wifi_kit_8": "d1_mini", + "huzzah": { + "LED": 0, + "LED_RED": 0, + "LED_BLUE": 2, + "D4": 4, + "D5": 5, + "D12": 12, + "D13": 13, + "D14": 14, + "D15": 15, + "D16": 16, + }, + "inventone": {}, + "modwifi": {}, + "nodemcu": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + "LED": 16, + }, + "nodemcuv2": "nodemcu", + "oak": { + "P0": 2, + "P1": 5, + "P2": 0, + "P3": 3, + "P4": 1, + "P5": 4, + "P6": 15, + "P7": 13, + "P8": 12, + "P9": 14, + "P10": 16, + "P11": 17, + "LED": 5, + }, + "phoenix_v1": {"LED": 16}, + "phoenix_v2": {"LED": 2}, + "sonoff_basic": {}, + "sonoff_s20": {}, + "sonoff_sv": {}, + "sonoff_th": {}, + "sparkfunBlynk": "thing", + "thing": {"LED": 5, "SDA": 2, "SCL": 14}, + "thingdev": "thing", + "wifi_slot": {"LED": 2}, + "wifiduino": { + "D0": 3, + "D1": 1, + "D2": 2, + "D3": 0, + "D4": 4, + "D5": 5, + "D6": 16, + "D7": 14, + "D8": 12, + "D9": 13, + "D10": 15, + "D11": 13, + "D12": 12, + "D13": 14, + }, + "wifinfo": { + "LED": 12, + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + }, + "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, + "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, + "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, +} + +FLASH_SIZE_1_MB = 2 ** 20 +FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 +FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB +FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB +FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB + +ESP8266_FLASH_SIZES = { + "d1": FLASH_SIZE_4_MB, + "d1_mini": FLASH_SIZE_4_MB, + "d1_mini_lite": FLASH_SIZE_1_MB, + "d1_mini_pro": FLASH_SIZE_16_MB, + "esp01": FLASH_SIZE_512_KB, + "esp01_1m": FLASH_SIZE_1_MB, + "esp07": FLASH_SIZE_4_MB, + "esp12e": FLASH_SIZE_4_MB, + "esp210": FLASH_SIZE_4_MB, + "esp8285": FLASH_SIZE_1_MB, + "esp_wroom_02": FLASH_SIZE_2_MB, + "espduino": FLASH_SIZE_4_MB, + "espectro": FLASH_SIZE_4_MB, + "espino": FLASH_SIZE_4_MB, + "espinotee": FLASH_SIZE_4_MB, + "espmxdevkit": FLASH_SIZE_1_MB, + "espresso_lite_v1": FLASH_SIZE_4_MB, + "espresso_lite_v2": FLASH_SIZE_4_MB, + "gen4iod": FLASH_SIZE_512_KB, + "heltec_wifi_kit_8": FLASH_SIZE_4_MB, + "huzzah": FLASH_SIZE_4_MB, + "inventone": FLASH_SIZE_4_MB, + "modwifi": FLASH_SIZE_2_MB, + "nodemcu": FLASH_SIZE_4_MB, + "nodemcuv2": FLASH_SIZE_4_MB, + "oak": FLASH_SIZE_4_MB, + "phoenix_v1": FLASH_SIZE_4_MB, + "phoenix_v2": FLASH_SIZE_4_MB, + "sonoff_basic": FLASH_SIZE_1_MB, + "sonoff_s20": FLASH_SIZE_1_MB, + "sonoff_sv": FLASH_SIZE_1_MB, + "sonoff_th": FLASH_SIZE_1_MB, + "sparkfunBlynk": FLASH_SIZE_4_MB, + "thing": FLASH_SIZE_512_KB, + "thingdev": FLASH_SIZE_512_KB, + "wifi_slot": FLASH_SIZE_1_MB, + "wifiduino": FLASH_SIZE_4_MB, + "wifinfo": FLASH_SIZE_1_MB, + "wio_link": FLASH_SIZE_4_MB, + "wio_node": FLASH_SIZE_4_MB, + "xinabox_cw01": FLASH_SIZE_4_MB, +} + +ESP8266_LD_SCRIPTS = { + FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), + FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), + FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), + FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), + FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), +} diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py new file mode 100644 index 0000000000..16a050360c --- /dev/null +++ b/esphome/components/esp8266/const.py @@ -0,0 +1,8 @@ +import esphome.codegen as cg + +KEY_ESP8266 = "esp8266" +KEY_BOARD = "board" +CONF_RESTORE_FROM_FLASH = "restore_from_flash" + +# esp8266 namespace is already defined by arduino, manually prefix esphome +esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp new file mode 100644 index 0000000000..b78600e7a3 --- /dev/null +++ b/esphome/components/esp8266/core.cpp @@ -0,0 +1,59 @@ +#ifdef USE_ESP8266 + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" +#include +#include + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::yield(); } +uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +uint32_t IRAM_ATTR HOT micros() { return ::micros(); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } +void arch_restart() { + ESP.restart(); // NOLINT(readability-static-accessed-through-instance) + // restart() doesn't always end execution + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} +void IRAM_ATTR HOT arch_feed_wdt() { + ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance) +} + +uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +uint32_t arch_get_cpu_cycle_count() { + return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance) +} +uint32_t arch_get_cpu_freq_hz() { return F_CPU; } + +void force_link_symbols() { + // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible + // with their settings - ESPHome uses a different settings system (that can also survive + // erases). So set magic bytes indicating all tasmota versions are supported. + // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support + // for Tasmota. + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380 + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654 + const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A}; + // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO) + volatile int x = 0; + x = TASMOTA_MAGIC_BYTES[x]; +} + +extern "C" void resetPins() { // NOLINT + // Added in framework 2.7.0 + // usually this sets up all pins to be in INPUT mode + // however, not strictly needed as we set up the pins properly + // ourselves and this causes pins to toggle during reboot. + force_link_symbols(); +} + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp new file mode 100644 index 0000000000..cb703c18e1 --- /dev/null +++ b/esphome/components/esp8266/gpio.cpp @@ -0,0 +1,96 @@ +#ifdef USE_ESP8266 + +#include "gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace esp8266 { + +static const char *const TAG = "esp8266"; + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + uint8_t arduino_mode = 0; + 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_ ? ONHIGH : ONLOW; + break; + case gpio::INTERRUPT_HIGH_LEVEL: + arduino_mode = inverted_ ? ONLOW : ONHIGH; + break; + } + + attachInterruptArg(pin_, func, arg, arduino_mode); +} +void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { + uint8_t mode; + if (flags == gpio::FLAG_INPUT) { + mode = INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + mode = OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + mode = INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + mode = INPUT_PULLDOWN_16; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + mode = OUTPUT_OPEN_DRAIN; + } else { + return; + } + pinMode(pin_, mode); // NOLINT +} + +std::string ESP8266GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool ESP8266GPIOPin::digital_read() { + return bool(digitalRead(pin_)) != inverted_; // NOLINT +} +void ESP8266GPIOPin::digital_write(bool value) { + digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT +} +void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } + +} // namespace esp8266 + +using namespace esp8266; + +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 ? 1 : 0); // NOLINT +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); + GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); +} + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h new file mode 100644 index 0000000000..0474d0baa6 --- /dev/null +++ b/esphome/components/esp8266/gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace esp8266 { + +class ESP8266GPIOPin : 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 esp8266 +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py new file mode 100644 index 0000000000..0ebfbd6f69 --- /dev/null +++ b/esphome/components/esp8266/gpio.py @@ -0,0 +1,170 @@ +import logging + +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +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 KEY_BOARD, KEY_ESP8266, esp8266_ns + + +_LOGGER = logging.getLogger(__name__) + + +ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) + + +def _lookup_pin(value): + board = CORE.data[KEY_ESP8266][KEY_BOARD] + board_pins = boards.ESP8266_BOARD_PINS.get(board, {}) + + # Resolved aliased board pins (shorthand when two boards have the same pin configuration) + while isinstance(board_pins, str): + board_pins = boards.ESP8266_BOARD_PINS[board_pins] + + if value in board_pins: + return board_pins[value] + if value in boards.ESP8266_BASE_PINS: + return boards.ESP8266_BASE_PINS[value] + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") + + +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): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return _lookup_pin(value) + + +_ESP_SDIO_PINS = { + 6: "Flash Clock", + 7: "Flash Data 0", + 8: "Flash Data 1", + 11: "Flash Command", +} + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > 17: + raise cv.Invalid(f"ESP8266: Invalid pin number: {value}") + if value in _ESP_SDIO_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP8266s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + ) + if 9 <= value <= 10: + _LOGGER.warning( + "ESP8266: Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", + value, + ) + return value + + +def validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + 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] + is_analog = mode[CONF_ANALOG] + + if (not is_analog) and num == 17: + raise cv.Invalid( + "GPIO17 (TOUT) is an analog-only pin on the ESP8266.", + [CONF_MODE], + ) + if is_analog and num != 17: + raise cv.Invalid( + "Only GPIO17 is analog-capable on ESP8266.", + [CONF_MODE, CONF_ANALOG], + ) + 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_pullup and num == 0: + raise cv.Invalid( + "GPIO Pin 0 does not support pullup pin mode. " + "Please choose another pin.", + [CONF_MODE, CONF_PULLUP], + ) + if is_pulldown and num != 16: + raise cv.Invalid("Only GPIO16 supports pulldown.", [CONF_MODE, CONF_PULLDOWN]) + + # (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_16 + (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 on ESP8266", + [CONF_MODE], + ) + + 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, + }, + validate_supports, +) + + +@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) +async def esp8266_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/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp new file mode 100644 index 0000000000..041736943b --- /dev/null +++ b/esphome/components/esp8266/preferences.cpp @@ -0,0 +1,270 @@ +#ifdef USE_ESP8266 + +#include +extern "C" { +#include "spi_flash.h" +} + +#include "preferences.h" +#include +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace esp8266 { + +static const char *const TAG = "esp8266.preferences"; + +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; +static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; + +#ifdef USE_ESP8266_PREFERENCES_FLASH +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; +#else +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; +#endif + +static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + *dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr) + return true; +} + +static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + if (index < 32 && s_prevent_write) { + return false; + } + + auto *ptr = &ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr) + *ptr = value; + return true; +} + +extern "C" uint32_t _SPIFFS_end; // NOLINT + +static const uint32_t get_esp8266_flash_sector() { + union { + uint32_t *ptr; + uint32_t uint; + } data{}; + data.ptr = &_SPIFFS_end; + return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE; +} +static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } + +template uint32_t calculate_crc(It first, It last, uint32_t type) { + uint32_t crc = type; + while (first != last) { + crc ^= (*first++ * 2654435769UL) >> 1; + } + return crc; +} + +static bool save_to_flash(size_t offset, const uint32_t *data, size_t len) { + for (uint32_t i = 0; i < len; i++) { + uint32_t j = offset + i; + if (j >= ESP8266_FLASH_STORAGE_SIZE) + return false; + uint32_t v = data[i]; + uint32_t *ptr = &s_flash_storage[j]; + if (*ptr != v) + s_flash_dirty = true; + *ptr = v; + } + return true; +} + +static bool load_from_flash(size_t offset, uint32_t *data, size_t len) { + for (size_t i = 0; i < len; i++) { + uint32_t j = offset + i; + if (j >= ESP8266_FLASH_STORAGE_SIZE) + return false; + data[i] = s_flash_storage[j]; + } + return true; +} + +static bool save_to_rtc(size_t offset, const uint32_t *data, size_t len) { + for (uint32_t i = 0; i < len; i++) + if (!esp_rtc_user_mem_write(offset + i, data[i])) + return false; + return true; +} + +static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { + for (uint32_t i = 0; i < len; i++) + if (!esp_rtc_user_mem_read(offset + i, &data[i])) + return false; + return true; +} + +class ESP8266PreferenceBackend : public ESPPreferenceBackend { + public: + size_t offset = 0; + uint32_t type = 0; + bool in_flash = false; + size_t length_words = 0; + + bool save(const uint8_t *data, size_t len) override { + if ((len + 3) / 4 != length_words) { + return false; + } + std::vector buffer; + buffer.resize(length_words + 1); + memcpy(buffer.data(), data, len); + buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type); + + if (in_flash) { + return save_to_flash(offset, buffer.data(), buffer.size()); + } else { + return save_to_rtc(offset, buffer.data(), buffer.size()); + } + } + bool load(uint8_t *data, size_t len) override { + if ((len + 3) / 4 != length_words) { + return false; + } + std::vector buffer; + buffer.resize(length_words + 1); + bool ret; + if (in_flash) { + ret = load_from_flash(offset, buffer.data(), buffer.size()); + } else { + ret = load_from_rtc(offset, buffer.data(), buffer.size()); + } + if (!ret) + return false; + + uint32_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); + if (buffer[buffer.size() - 1] != crc) { + return false; + } + + memcpy(data, buffer.data(), len); + return true; + } +}; + +class ESP8266Preferences : public ESPPreferences { + public: + uint32_t current_offset = 0; + uint32_t current_flash_offset = 0; // in words + + void setup() { + s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT + ESP_LOGVV(TAG, "Loading preferences from flash..."); + + { + InterruptLock lock; + spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + uint32_t length_words = (length + 3) / 4; + if (in_flash) { + uint32_t start = current_flash_offset; + uint32_t end = start + length_words + 1; + if (end > ESP8266_FLASH_STORAGE_SIZE) + return {}; + auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = start; + pref->type = type; + pref->length_words = length_words; + pref->in_flash = true; + current_flash_offset = end; + return {pref}; + } + + uint32_t start = current_offset; + uint32_t end = start + length_words + 1; + bool in_normal = start < 96; + // Normal: offset 0-95 maps to RTC offset 32 - 127, + // Eboot: offset 96-127 maps to RTC offset 0 - 31 words + if (in_normal && end > 96) { + // start is in normal but end is not -> switch to Eboot + current_offset = start = 96; + end = start + length_words + 1; + in_normal = false; + } + + if (end > 128) { + // Doesn't fit in data, return uninitialized preference obj. + return {}; + } + + uint32_t rtc_offset = in_normal ? start + 32 : start - 96; + + auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = rtc_offset; + pref->type = type; + pref->length_words = length_words; + pref->in_flash = false; + current_offset += length_words + 1; + return pref; + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { +#ifdef USE_ESP8266_PREFERENCES_FLASH + return make_preference(length, type, true); +#else + return make_preference(length, type, false); +#endif + } + + bool sync() override { + if (!s_flash_dirty) + return true; + if (s_prevent_write) + return false; + + ESP_LOGD(TAG, "Saving preferences to flash..."); + SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res == SPI_FLASH_RESULT_OK) { + write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } + } + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); + return false; + } + if (write_res != SPI_FLASH_RESULT_OK) { + ESP_LOGV(TAG, "Write ESP8266 flash failed!"); + return false; + } + + s_flash_dirty = false; + return true; + } +}; + +void setup_preferences() { + auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) + pref->setup(); + global_preferences = pref; +} +void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } + +} // namespace esp8266 + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h new file mode 100644 index 0000000000..edec915794 --- /dev/null +++ b/esphome/components/esp8266/preferences.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_ESP8266 + +namespace esphome { +namespace esp8266 { + +void setup_preferences(); +void preferences_prevent_write(bool prevent); + +} // namespace esp8266 +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 37a9f3efbf..e472edf2a7 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -1,9 +1,12 @@ +#ifdef USE_ESP8266 + #include "esp8266_pwm.h" +#include "esphome/core/macros.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error ESP8266 PWM requires at least arduino_core_version 2.4.0 +#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error ESP8266 PWM requires at least arduino_version 2.4.0 #endif #include @@ -54,3 +57,5 @@ void HOT ESP8266PWM::write_state(float state) { } // namespace esp8266_pwm } // namespace esphome + +#endif diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 661db6611f..79530aacd4 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -1,7 +1,9 @@ #pragma once +#ifdef USE_ESP8266 + #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" @@ -10,7 +12,7 @@ namespace esp8266_pwm { class ESP8266PWM : public output::FloatOutput, public Component { public: - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically update frequency void update_frequency(float frequency) override { @@ -27,7 +29,7 @@ class ESP8266PWM : public output::FloatOutput, public Component { protected: void write_state(float state) override; - GPIOPin *pin_; + InternalGPIOPin *pin_; float frequency_{1000.0}; /// Cache last output level for dynamic frequency updating float last_output_{0.0}; @@ -48,3 +50,5 @@ template class SetFrequencyAction : public Action { } // namespace esp8266_pwm } // namespace esphome + +#endif diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index 770d400013..3d52e5af16 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -7,10 +7,9 @@ from esphome.const import ( CONF_ID, CONF_NUMBER, CONF_PIN, - ESP_PLATFORM_ESP8266, ) -ESP_PLATFORMS = [ESP_PLATFORM_ESP8266] +DEPENDENCIES = ["esp8266"] def valid_pwm_pin(value): diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 94c9ddd2e9..bbf64a3cd1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,7 +1,6 @@ from esphome import pins import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components.network import add_mdns_library from esphome.const import ( CONF_DOMAIN, CONF_ID, @@ -9,17 +8,16 @@ from esphome.const import ( CONF_STATIC_IP, CONF_TYPE, CONF_USE_ADDRESS, - ESP_PLATFORM_ESP32, - CONF_ENABLE_MDNS, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, CONF_DNS2, ) from esphome.core import CORE, coroutine_with_priority +from esphome.components.network import IPAddress CONFLICTS_WITH = ["wifi"] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] ethernet_ns = cg.esphome_ns.namespace("ethernet") @@ -55,7 +53,6 @@ MANUAL_IP_SCHEMA = cv.Schema( ) EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) -IPAddress = cg.global_ns.class_("IPAddress") ManualIP = ethernet_ns.struct("ManualIP") @@ -74,23 +71,24 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(EthernetComponent), cv.Required(CONF_TYPE): cv.enum(ETHERNET_TYPES, upper=True), - cv.Required(CONF_MDC_PIN): pins.output_pin, - cv.Required(CONF_MDIO_PIN): pins.input_output_pin, + 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( CLK_MODES, upper=True, space="_" ), cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, - cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional("hostname"): cv.invalid( - "The hostname option has been removed in 1.11.0" + 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), _validate, + cv.only_with_arduino, ) @@ -126,5 +124,5 @@ async def to_code(config): cg.add_define("USE_ETHERNET") - if config[CONF_ENABLE_MDNS]: - add_mdns_library() + if CORE.is_esp32: + cg.add_library("WiFi", None) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 22007caafa..d55db0a7d8 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -3,27 +3,34 @@ #include "esphome/core/util.h" #include "esphome/core/application.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO #include #include #include -/// Macro for IDF version comparision +/// Macro for IDF version comparison #ifndef ESP_IDF_VERSION_VAL #define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) #endif // Defined in WiFiGeneric.cpp, sets global initialized flag, starts network event task queue and calls // tcpip_adapter_init() -extern void tcpipInit(); +extern void tcpipInit(); // NOLINT(readability-identifier-naming) namespace esphome { namespace ethernet { static const char *const TAG = "ethernet"; -EthernetComponent *global_eth_component; +EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#define ESPHL_ERROR_CHECK(err, message) \ + if ((err) != ESP_OK) { \ + ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ + this->mark_failed(); \ + return; \ + } EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { @@ -36,38 +43,77 @@ void EthernetComponent::setup() { this->power_pin_->setup(); } - this->start_connect_(); + switch (this->type_) { + case ETHERNET_TYPE_LAN8720: { + memcpy(&this->eth_config_, &phy_lan8720_default_ethernet_config, sizeof(eth_config_t)); + break; + } + case ETHERNET_TYPE_TLK110: { + memcpy(&this->eth_config_, &phy_tlk110_default_ethernet_config, sizeof(eth_config_t)); + break; + } + default: { + this->mark_failed(); + return; + } + } -#ifdef USE_MDNS - network_setup_mdns(); -#endif + this->eth_config_.phy_addr = static_cast(this->phy_addr_); + this->eth_config_.clock_mode = this->clk_mode_; + this->eth_config_.gpio_config = EthernetComponent::eth_phy_config_gpio; + this->eth_config_.tcpip_input = tcpip_adapter_eth_input; + + if (this->power_pin_ != nullptr) { + this->orig_power_enable_fun_ = this->eth_config_.phy_power_enable; + this->eth_config_.phy_power_enable = EthernetComponent::eth_phy_power_enable; + } + + tcpipInit(); + + esp_err_t err; + err = esp_eth_init(&this->eth_config_); + ESPHL_ERROR_CHECK(err, "ETH init error"); + err = esp_eth_enable(); + ESPHL_ERROR_CHECK(err, "ETH enable error"); } void EthernetComponent::loop() { const uint32_t now = millis(); - if (!this->connected_ && !this->last_connected_ && now - this->connect_begin_ > 15000) { - ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); - this->start_connect_(); - return; + + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting ethernet connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped ethernet connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected via Ethernet!"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped ethernet connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection via Ethernet lost! Re-connecting..."); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; } - - if (this->connected_ == this->last_connected_) - // nothing changed - return; - - if (this->connected_) { - // connection established - ESP_LOGI(TAG, "Connected via Ethernet!"); - this->dump_connect_params_(); - this->status_clear_warning(); - } else { - // connection lost - ESP_LOGW(TAG, "Connection via Ethernet lost! Re-connecting..."); - this->start_connect_(); - } - - this->last_connected_ = this->connected_; - - network_tick_mdns(); } void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, "Ethernet:"); @@ -79,10 +125,10 @@ void EthernetComponent::dump_config() { } float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } bool EthernetComponent::can_proceed() { return this->is_connected(); } -IPAddress EthernetComponent::get_ip_address() { +network::IPAddress EthernetComponent::get_ip_address() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); - return IPAddress(ip.ip.addr); + return {ip.ip.addr}; } void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_info_t info) { @@ -91,9 +137,11 @@ void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_inf switch (event) { case SYSTEM_EVENT_ETH_START: event_name = "ETH started"; + this->started_ = true; break; case SYSTEM_EVENT_ETH_STOP: event_name = "ETH stopped"; + this->started_ = false; this->connected_ = false; break; case SYSTEM_EVENT_ETH_CONNECTED: @@ -114,62 +162,13 @@ void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_inf ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); } -#define ESPHL_ERROR_CHECK(err, message) \ - if (err != ESP_OK) { \ - ESP_LOGE(TAG, message ": %d", err); \ - this->mark_failed(); \ - return; \ - } - void EthernetComponent::start_connect_() { this->connect_begin_ = millis(); this->status_set_warning(); esp_err_t err; - if (this->initialized_) { - // already initialized - err = esp_eth_enable(); - ESPHL_ERROR_CHECK(err, "ETH enable error"); - return; - } - - switch (this->type_) { - case ETHERNET_TYPE_LAN8720: { - memcpy(&this->eth_config, &phy_lan8720_default_ethernet_config, sizeof(eth_config_t)); - break; - } - case ETHERNET_TYPE_TLK110: { - memcpy(&this->eth_config, &phy_tlk110_default_ethernet_config, sizeof(eth_config_t)); - break; - } - default: { - this->mark_failed(); - return; - } - } - - this->eth_config.phy_addr = static_cast(this->phy_addr_); - this->eth_config.clock_mode = this->clk_mode_; - this->eth_config.gpio_config = EthernetComponent::eth_phy_config_gpio_; - this->eth_config.tcpip_input = tcpip_adapter_eth_input; - - if (this->power_pin_ != nullptr) { - this->orig_power_enable_fun_ = this->eth_config.phy_power_enable; - this->eth_config.phy_power_enable = EthernetComponent::eth_phy_power_enable_; - } - - tcpipInit(); - - err = esp_eth_init(&this->eth_config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "ETH init error: %d", err); - this->mark_failed(); - return; - } - - this->initialized_ = true; - - tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); + err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); + ESPHL_ERROR_CHECK(err, "ETH set hostname error"); tcpip_adapter_ip_info_t info; if (this->manual_ip_.has_value()) { @@ -210,24 +209,24 @@ void EthernetComponent::start_connect_() { this->connect_begin_ = millis(); this->status_set_warning(); } -void EthernetComponent::eth_phy_config_gpio_() { +void EthernetComponent::eth_phy_config_gpio() { phy_rmii_configure_data_interface_pins(); phy_rmii_smi_configure_pins(global_eth_component->mdc_pin_, global_eth_component->mdio_pin_); } -void EthernetComponent::eth_phy_power_enable_(bool enable) { +void EthernetComponent::eth_phy_power_enable(bool enable) { global_eth_component->power_pin_->digital_write(enable); // power up takes some time, datasheet says max 300µs delay(1); global_eth_component->orig_power_enable_fun_(enable); } -bool EthernetComponent::is_connected() { return this->connected_ && this->last_connected_; } +bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } void EthernetComponent::dump_connect_params_() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); - ESP_LOGCONFIG(TAG, " IP Address: %s", IPAddress(ip.ip.addr).toString().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(ip.ip.addr).str().c_str()); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); - ESP_LOGCONFIG(TAG, " Subnet: %s", IPAddress(ip.netmask.addr).toString().c_str()); - ESP_LOGCONFIG(TAG, " Gateway: %s", IPAddress(ip.gw.addr).toString().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()); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 4) const ip_addr_t *dns_ip1 = dns_getserver(0); @@ -238,14 +237,14 @@ void EthernetComponent::dump_connect_params_() { ip_addr_t tmp_ip2 = dns_getserver(1); const ip_addr_t *dns_ip2 = &tmp_ip2; #endif - ESP_LOGCONFIG(TAG, " DNS1: %s", IPAddress(dns_ip1->u_addr.ip4.addr).toString().c_str()); - ESP_LOGCONFIG(TAG, " DNS2: %s", IPAddress(dns_ip2->u_addr.ip4.addr).toString().c_str()); + 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()); uint8_t mac[6]; esp_eth_get_mac(mac); ESP_LOGCONFIG(TAG, " MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(this->eth_config.phy_get_duplex_mode())); - ESP_LOGCONFIG(TAG, " Link Up: %s", YESNO(this->eth_config.phy_check_link())); - ESP_LOGCONFIG(TAG, " Link Speed: %u", this->eth_config.phy_get_speed_mode() ? 100 : 10); + ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(this->eth_config_.phy_get_duplex_mode())); + ESP_LOGCONFIG(TAG, " Link Up: %s", YESNO(this->eth_config_.phy_check_link())); + ESP_LOGCONFIG(TAG, " Link Speed: %u", this->eth_config_.phy_get_speed_mode() ? 100 : 10); } void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } void EthernetComponent::set_power_pin(GPIOPin *power_pin) { this->power_pin_ = power_pin; } @@ -253,7 +252,7 @@ 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(eth_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } -void EthernetComponent::set_manual_ip(ManualIP manual_ip) { this->manual_ip_ = manual_ip; } +void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } std::string EthernetComponent::get_use_address() const { if (this->use_address_.empty()) { return App.get_name() + ".local"; @@ -265,4 +264,4 @@ void EthernetComponent::set_use_address(const std::string &use_address) { this-> } // namespace ethernet } // namespace esphome -#endif +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 2dbd8ccd9d..abe1c62030 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -1,9 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#ifdef ARDUINO_ARCH_ESP32 +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/network/ip_address.h" #include "esp_eth.h" #include @@ -19,11 +20,17 @@ enum EthernetType { }; struct ManualIP { - IPAddress static_ip; - IPAddress gateway; - IPAddress subnet; - IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. - IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. + network::IPAddress static_ip; + network::IPAddress gateway; + network::IPAddress subnet; + network::IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. + network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. +}; + +enum class EthernetComponentState { + STOPPED, + CONNECTING, + CONNECTED, }; class EthernetComponent : public Component { @@ -42,9 +49,9 @@ class EthernetComponent : public Component { void set_mdio_pin(uint8_t mdio_pin); void set_type(EthernetType type); void set_clk_mode(eth_clock_mode_t clk_mode); - void set_manual_ip(ManualIP manual_ip); + void set_manual_ip(const ManualIP &manual_ip); - IPAddress get_ip_address(); + network::IPAddress get_ip_address(); std::string get_use_address() const; void set_use_address(const std::string &use_address); @@ -53,8 +60,8 @@ class EthernetComponent : public Component { void start_connect_(); void dump_connect_params_(); - static void eth_phy_config_gpio_(); - static void eth_phy_power_enable_(bool enable); + static void eth_phy_config_gpio(); + static void eth_phy_power_enable(bool enable); std::string use_address_; uint8_t phy_addr_{0}; @@ -65,17 +72,18 @@ class EthernetComponent : public Component { eth_clock_mode_t clk_mode_{ETH_CLOCK_GPIO0_IN}; optional manual_ip_{}; - bool initialized_{false}; + bool started_{false}; bool connected_{false}; - bool last_connected_{false}; + EthernetComponentState state_{EthernetComponentState::STOPPED}; uint32_t connect_begin_; - eth_config_t eth_config; + eth_config_t eth_config_; eth_phy_power_enable_func orig_power_enable_fun_; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; } // namespace ethernet } // namespace esphome -#endif +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp index 9db181fbee..3083cf429c 100644 --- a/esphome/components/exposure_notifications/exposure_notifications.cpp +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace exposure_notifications { diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h index 6b9f61b2a0..f7383c28d9 100644 --- a/esphome/components/exposure_notifications/exposure_notifications.h +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace exposure_notifications { diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 1602ac3b07..110a8d95ed 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,13 +1,12 @@ import re import logging from pathlib import Path -import subprocess -import hashlib -import datetime import esphome.config_validation as cv from esphome.const import ( CONF_COMPONENTS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_URL, CONF_TYPE, @@ -15,7 +14,7 @@ from esphome.const import ( CONF_PATH, ) from esphome.core import CORE -from esphome import loader +from esphome import git, loader _LOGGER = logging.getLogger(__name__) @@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS TYPE_GIT = "git" TYPE_LOCAL = "local" -CONF_REFRESH = "refresh" -CONF_REF = "ref" - - -def validate_git_ref(value): - if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: - raise cv.Invalid("Not a valid git ref") - return value GIT_SCHEMA = { cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_REF): validate_git_ref, + cv.Optional(CONF_REF): cv.git_ref, } LOCAL_SCHEMA = { cv.Required(CONF_PATH): cv.directory, @@ -68,14 +59,6 @@ def validate_source_shorthand(value): return SOURCE_SCHEMA(conf) -def validate_refresh(value: str): - if value.lower() == "always": - return validate_refresh("0s") - if value.lower() == "never": - return validate_refresh("1000y") - return cv.positive_time_period_seconds(value) - - SOURCE_SCHEMA = cv.Any( validate_source_shorthand, cv.typed_schema( @@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any( CONFIG_SCHEMA = cv.ensure_list( { cv.Required(CONF_SOURCE): SOURCE_SCHEMA, - cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( "all", cv.ensure_list(cv.string) ), @@ -102,62 +85,33 @@ async def to_code(config): pass -def _compute_destination_path(key: str) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN - h = hashlib.new("sha256") - h.update(key.encode()) - return base_dir / h.hexdigest()[:8] +def _process_git_config(config: dict, refresh) -> str: + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=refresh, + domain=DOMAIN, + ) + if (repo_dir / "esphome" / "components").is_dir(): + components_dir = repo_dir / "esphome" / "components" + elif (repo_dir / "components").is_dir(): + components_dir = repo_dir / "components" + else: + raise cv.Invalid( + "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder" + ) -def _handle_git_response(ret): - if ret.returncode != 0 and ret.stderr: - err_str = ret.stderr.decode("utf-8") - lines = [x.strip() for x in err_str.splitlines()] - if lines[-1].startswith("fatal:"): - raise cv.Invalid(lines[-1][len("fatal: ") :]) - raise cv.Invalid(err_str) + return components_dir def _process_single_config(config: dict): conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: - key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}" - repo_dir = _compute_destination_path(key) - if not repo_dir.is_dir(): - cmd = ["git", "clone", "--depth=1"] - if CONF_REF in conf: - cmd += ["--branch", conf[CONF_REF]] - cmd += [conf[CONF_URL], str(repo_dir)] - ret = subprocess.run(cmd, capture_output=True, check=False) - _handle_git_response(ret) - - else: - # Check refresh needed - file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") - # On first clone, FETCH_HEAD does not exists - if not file_timestamp.exists(): - file_timestamp = Path(repo_dir / ".git" / "HEAD") - age = datetime.datetime.now() - datetime.datetime.fromtimestamp( - file_timestamp.stat().st_mtime + with cv.prepend_path([CONF_SOURCE]): + components_dir = _process_git_config( + config[CONF_SOURCE], config[CONF_REFRESH] ) - if age.seconds > config[CONF_REFRESH].total_seconds: - _LOGGER.info("Executing git pull %s", key) - cmd = ["git", "pull"] - ret = subprocess.run( - cmd, cwd=repo_dir, capture_output=True, check=False - ) - _handle_git_response(ret) - - if (repo_dir / "esphome" / "components").is_dir(): - components_dir = repo_dir / "esphome" / "components" - elif (repo_dir / "components").is_dir(): - components_dir = repo_dir / "components" - else: - raise cv.Invalid( - "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder", - [CONF_SOURCE], - ) - elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) else: diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 90b1e4ace9..81597f3466 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -1,5 +1,6 @@ #include "ezo.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ezo { @@ -24,7 +25,7 @@ void EZOSensor::update() { return; } uint8_t c = 'R'; - this->write_bytes_raw(&c, 1); + this->write(&c, 1); this->state_ |= EZO_STATE_WAIT; this->start_time_ = millis(); this->wait_time_ = 900; @@ -35,7 +36,7 @@ void EZOSensor::loop() { if (!(this->state_ & EZO_STATE_WAIT)) { if (this->state_ & EZO_STATE_SEND_TEMP) { int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_); - this->write_bytes_raw(buf, len); + this->write(buf, len); this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP; this->start_time_ = millis(); this->wait_time_ = 300; diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index d1f43467ed..52bec3b5b6 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -5,36 +5,50 @@ from esphome.automation import maybe_simple_id from esphome.components import mqtt from esphome.const import ( CONF_ID, - CONF_INTERNAL, CONF_MQTT_ID, CONF_OSCILLATING, CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_SPEED, + CONF_SPEED_LEVEL_COMMAND_TOPIC, + CONF_SPEED_LEVEL_STATE_TOPIC, CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, - CONF_NAME, + CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, + CONF_DIRECTION, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True fan_ns = cg.esphome_ns.namespace("fan") -FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component) +FanState = fan_ns.class_("FanState", cg.EntityBase, cg.Component) MakeFan = cg.Application.struct("MakeFan") +FanDirection = fan_ns.enum("FanDirection") +FAN_DIRECTION_ENUM = { + "FORWARD": FanDirection.FAN_DIRECTION_FORWARD, + "REVERSE": FanDirection.FAN_DIRECTION_REVERSE, +} + # Actions TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) ToggleAction = fan_ns.class_("ToggleAction", automation.Action) +CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) +FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) -FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +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(FanState), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), @@ -44,6 +58,12 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( 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 ), @@ -60,14 +80,17 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( 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), + } + ), } ) async def setup_fan_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) + await setup_entity(var, config) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -85,6 +108,18 @@ async def setup_fan_core_(var, config): 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: @@ -98,6 +133,9 @@ async def setup_fan_core_(var, config): 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_SPEED_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): @@ -141,6 +179,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(FanState), cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), + cv.Optional(CONF_DIRECTION): cv.templatable( + cv.enum(FAN_DIRECTION_ENUM, upper=True) + ), } ), ) @@ -153,9 +194,41 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): if CONF_SPEED in config: template_ = await cg.templatable(config[CONF_SPEED], args, int) cg.add(var.set_speed(template_)) + if CONF_DIRECTION in config: + template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection) + cg.add(var.set_direction(template_)) return var +@automation.register_action("fan.cycle_speed", CycleSpeedAction, FAN_ACTION_SCHEMA) +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) + + +@automation.register_condition( + "fan.is_on", + FanIsOnCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +@automation.register_condition( + "fan.is_off", + FanIsOffCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +async def fan_is_on_off_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_define("USE_FAN") diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index fbfc71c720..608f772b75 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -13,6 +13,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(bool, oscillating) TEMPLATABLE_VALUE(int, speed) + TEMPLATABLE_VALUE(FanDirection, direction) void play(Ts... x) override { auto call = this->state_->turn_on(); @@ -22,6 +23,9 @@ template class TurnOnAction : public Action { if (this->speed_.has_value()) { call.set_speed(this->speed_.value(x...)); } + if (this->direction_.has_value()) { + call.set_direction(this->direction_.value(x...)); + } call.perform(); } @@ -46,6 +50,59 @@ template class ToggleAction : public Action { FanState *state_; }; +template class CycleSpeedAction : public Action { + public: + explicit CycleSpeedAction(FanState *state) : state_(state) {} + + 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 + speed = 1; + auto call = this->state_->turn_off(); + call.set_speed(speed); + call.perform(); + } else { + auto call = this->state_->turn_on(); + call.set_speed(speed); + call.perform(); + } + } else { + // fan was off, so set speed to 1 + auto call = this->state_->turn_on(); + call.set_speed(1); + call.perform(); + } + } else { + // fan doesn't support speed counts, so toggle + this->state_->toggle().perform(); + } + } + + FanState *state_; +}; + +template class FanIsOnCondition : public Condition { + public: + explicit FanIsOnCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return this->state_->state; } + + protected: + FanState *state_; +}; +template class FanIsOffCondition : public Condition { + public: + explicit FanIsOffCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return !this->state_->state; } + + protected: + FanState *state_; +}; + class FanTurnOnTrigger : public Trigger<> { public: FanTurnOnTrigger(FanState *state) { @@ -82,5 +139,23 @@ class FanTurnOffTrigger : public Trigger<> { bool last_on_; }; +class FanSpeedSetTrigger : public Trigger<> { + public: + FanSpeedSetTrigger(FanState *state) { + state->add_on_state_callback([this, state]() { + auto speed = state->speed; + auto should_trigger = speed != !this->last_speed_; + this->last_speed_ = speed; + if (should_trigger) { + this->trigger(); + } + }); + this->last_speed_ = state->speed; + } + + protected: + int last_speed_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp index be16e6bb64..34883617e6 100644 --- a/esphome/components/fan/fan_helpers.cpp +++ b/esphome/components/fan/fan_helpers.cpp @@ -4,9 +4,12 @@ namespace esphome { namespace fan { +// This whole file is deprecated, don't warn about usage of deprecated types in here. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); - const auto legacy_level = static_cast(clamp(ceilf(speed_ratio * 3), 1, 3)); + const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); return static_cast(legacy_level - 1); } diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h index 138aa5bca3..009505601e 100644 --- a/esphome/components/fan/fan_helpers.h +++ b/esphome/components/fan/fan_helpers.h @@ -4,8 +4,16 @@ namespace esphome { namespace fan { +// Shut-up about usage of deprecated FanSpeed for a bit. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); +ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); +#pragma GCC diagnostic pop + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index 7cfe3afef7..6ff4d3a833 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -12,7 +12,7 @@ void FanState::set_traits(const FanTraits &traits) { this->traits_ = traits; } void FanState::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -FanState::FanState(const std::string &name) : Nameable(name) {} +FanState::FanState(const std::string &name) : EntityBase(name) {} FanStateCall FanState::turn_on() { return this->make_call().set_state(true); } FanStateCall FanState::turn_off() { return this->make_call().set_state(false); } @@ -27,7 +27,7 @@ struct FanStateRTCState { }; void FanState::setup() { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); FanStateRTCState recovered{}; if (!this->rtc_.load(&recovered)) return; @@ -39,7 +39,7 @@ void FanState::setup() { call.set_direction(recovered.direction); call.perform(); } -float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; } uint32_t FanState::hash_base() { return 418001110UL; } void FanStateCall::perform() const { @@ -54,7 +54,7 @@ void FanStateCall::perform() const { } if (this->speed_.has_value()) { const int speed_count = this->state_->get_traits().supported_speed_count(); - this->state_->speed = static_cast(clamp(*this->speed_, 1, speed_count)); + this->state_->speed = clamp(*this->speed_, 1, speed_count); } FanStateRTCState saved{}; @@ -67,6 +67,8 @@ void FanStateCall::perform() const { this->state_->state_callback_.call(); } +// This whole method is deprecated, don't warn about usage of deprecated methods inside of it. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" FanStateCall &FanStateCall::set_speed(const char *legacy_speed) { const auto supported_speed_count = this->state_->get_traits().supported_speed_count(); if (strcasecmp(legacy_speed, "low") == 0) { diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index a0dda4083a..c5a6f59ac4 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/core/log.h" @@ -10,7 +11,7 @@ namespace esphome { namespace fan { /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum FanSpeed { +enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { FAN_SPEED_LOW = 0, ///< The fan is running on low speed. FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. @@ -45,6 +46,7 @@ class FanStateCall { this->speed_ = speed; return *this; } + ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") FanStateCall &set_speed(const char *legacy_speed); FanStateCall &set_direction(FanDirection direction) { this->direction_ = direction; @@ -65,7 +67,7 @@ class FanStateCall { optional direction_{}; }; -class FanState : public Nameable, public Component { +class FanState : public EntityBase, public Component { public: FanState() = default; /// Construct the fan state with name. diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index f2d0bb1f38..62de036e62 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -44,5 +44,5 @@ async def new_fastled_light(config): # https://github.com/FastLED/FastLED/blob/master/library.json # 3.3.3 has an issue on ESP32 with RMT and fastled_clockless: # https://github.com/esphome/issues/issues/1375 - cg.add_library("FastLED", "3.3.2") + cg.add_library("fastled/FastLED", "3.3.2") return var diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 4d791f5709..486364d0c0 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "fastled_light.h" #include "esphome/core/log.h" @@ -10,7 +12,7 @@ void FastLEDLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up FastLED light..."); this->controller_->init(); this->controller_->setLeds(this->leds_, this->num_leds_); - this->effect_data_ = new uint8_t[this->num_leds_]; + this->effect_data_ = new uint8_t[this->num_leds_]; // NOLINT if (!this->max_refresh_rate_.has_value()) { this->set_max_refresh_rate(this->controller_->getMaxRefreshRate()); } @@ -20,13 +22,12 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, " Num LEDs: %u", this->num_leds_); ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); } -void FastLEDLightOutput::loop() { - if (!this->should_show_()) - return; - - uint32_t now = micros(); +void FastLEDLightOutput::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; @@ -38,3 +39,5 @@ void FastLEDLightOutput::loop() { } // namespace fastled_base } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 59d143dbef..26f0f33d2a 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/light/addressable_light.h" @@ -30,7 +32,7 @@ class FastLEDLightOutput : public light::AddressableLight { CLEDController &add_leds(CLEDController *controller, int num_leds) { this->controller_ = controller; this->num_leds_ = num_leds; - this->leds_ = new CRGB[num_leds]; + this->leds_ = new CRGB[num_leds]; // NOLINT for (int i = 0; i < this->num_leds_; i++) this->leds_[i] = CRGB::Black; @@ -42,33 +44,33 @@ class FastLEDLightOutput : public light::AddressableLight { CLEDController &add_leds(int num_leds) { switch (CHIPSET) { case LPD8806: { - static LPD8806Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static LPD8806Controller controller; + return add_leds(&controller, num_leds); } case WS2801: { - static WS2801Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2801Controller controller; + return add_leds(&controller, num_leds); } case WS2803: { - static WS2803Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2803Controller controller; + return add_leds(&controller, num_leds); } case SM16716: { - static SM16716Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SM16716Controller controller; + return add_leds(&controller, num_leds); } case P9813: { - static P9813Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static P9813Controller controller; + return add_leds(&controller, num_leds); } case DOTSTAR: case APA102: { - static APA102Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static APA102Controller controller; + return add_leds(&controller, num_leds); } case SK9822: { - static SK9822Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SK9822Controller controller; + return add_leds(&controller, num_leds); } } } @@ -76,33 +78,33 @@ class FastLEDLightOutput : public light::AddressableLight { template CLEDController &add_leds(int num_leds) { switch (CHIPSET) { case LPD8806: { - static LPD8806Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static LPD8806Controller controller; + return add_leds(&controller, num_leds); } case WS2801: { - static WS2801Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2801Controller controller; + return add_leds(&controller, num_leds); } case WS2803: { - static WS2803Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2803Controller controller; + return add_leds(&controller, num_leds); } case SM16716: { - static SM16716Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SM16716Controller controller; + return add_leds(&controller, num_leds); } case P9813: { - static P9813Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static P9813Controller controller; + return add_leds(&controller, num_leds); } case DOTSTAR: case APA102: { - static APA102Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static APA102Controller controller; + return add_leds(&controller, num_leds); } case SK9822: { - static SK9822Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SK9822Controller controller; + return add_leds(&controller, num_leds); } } } @@ -111,33 +113,33 @@ class FastLEDLightOutput : public light::AddressableLight { CLEDController &add_leds(int num_leds) { switch (CHIPSET) { case LPD8806: { - static LPD8806Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static LPD8806Controller controller; + return add_leds(&controller, num_leds); } case WS2801: { - static WS2801Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2801Controller controller; + return add_leds(&controller, num_leds); } case WS2803: { - static WS2803Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static WS2803Controller controller; + return add_leds(&controller, num_leds); } case SM16716: { - static SM16716Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SM16716Controller controller; + return add_leds(&controller, num_leds); } case P9813: { - static P9813Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static P9813Controller controller; + return add_leds(&controller, num_leds); } case DOTSTAR: case APA102: { - static APA102Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static APA102Controller controller; + return add_leds(&controller, num_leds); } case SK9822: { - static SK9822Controller CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static SK9822Controller controller; + return add_leds(&controller, num_leds); } } } @@ -145,30 +147,30 @@ class FastLEDLightOutput : public light::AddressableLight { #ifdef FASTLED_HAS_CLOCKLESS template class CHIPSET, uint8_t DATA_PIN, EOrder RGB_ORDER> CLEDController &add_leds(int num_leds) { - static CHIPSET CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static CHIPSET controller; + return add_leds(&controller, num_leds); } template class CHIPSET, uint8_t DATA_PIN> CLEDController &add_leds(int num_leds) { - static CHIPSET CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static CHIPSET controller; + return add_leds(&controller, num_leds); } template class CHIPSET, uint8_t DATA_PIN> CLEDController &add_leds(int num_leds) { - static CHIPSET CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static CHIPSET controller; + return add_leds(&controller, num_leds); } #endif template class CHIPSET, EOrder RGB_ORDER> CLEDController &add_leds(int num_leds) { - static CHIPSET CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static CHIPSET controller; + return add_leds(&controller, num_leds); } template class CHIPSET> CLEDController &add_leds(int num_leds) { - static CHIPSET CONTROLLER; - return add_leds(&CONTROLLER, num_leds); + static CHIPSET controller; + return add_leds(&controller, num_leds); } #ifdef FASTLED_HAS_BLOCKLESS @@ -208,13 +210,12 @@ class FastLEDLightOutput : public light::AddressableLight { // (In most use cases you won't need these) light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void setup() override; void dump_config() override; - void loop() override; + void write_state(light::LightState *state) override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void clear_effect_data() override { @@ -238,3 +239,5 @@ class FastLEDLightOutput : public light::AddressableLight { } // namespace fastled_base } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index cfc62e930b..acf9488ae3 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -31,6 +31,7 @@ CHIPSETS = [ "GW6205_400", "LPD1886", "LPD1886_8BIT", + "SM16703", ] @@ -44,10 +45,11 @@ CONFIG_SCHEMA = cv.All( fastled_base.BASE_SCHEMA.extend( { cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), - cv.Required(CONF_PIN): pins.output_pin, + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, } ), _validate, + cv.only_with_arduino, ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index d6ba0e8358..a729fc015a 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -24,13 +24,16 @@ CHIPSETS = [ "DOTSTAR", ] -CONFIG_SCHEMA = fastled_base.BASE_SCHEMA.extend( - { - cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), - cv.Required(CONF_DATA_PIN): pins.output_pin, - cv.Required(CONF_CLOCK_PIN): pins.output_pin, - cv.Optional(CONF_DATA_RATE): cv.frequency, - } +CONFIG_SCHEMA = cv.All( + fastled_base.BASE_SCHEMA.extend( + { + cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Required(CONF_DATA_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CLOCK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_DATA_RATE): cv.frequency, + } + ), + cv.only_with_arduino, ) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index b0c0be59af..be17e29de3 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -15,7 +15,7 @@ void FingerprintGrowComponent::update() { } if (this->sensing_pin_ != nullptr) { - if (this->sensing_pin_->digital_read() == HIGH) { + if (this->sensing_pin_->digital_read()) { ESP_LOGV(TAG, "No touch sensing"); this->waiting_removal_ = false; return; diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index f8a44eb0da..f359a10348 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -8,15 +8,12 @@ from esphome.const import ( CONF_LAST_FINGER_ID, CONF_SECURITY_LEVEL, CONF_STATUS, - DEVICE_CLASS_EMPTY, ICON_ACCOUNT, ICON_ACCOUNT_CHECK, ICON_DATABASE, - ICON_EMPTY, ICON_FINGERPRINT, ICON_SECURITY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent @@ -26,22 +23,33 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( - UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FINGERPRINT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( - UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_DATABASE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( - UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_SECURITY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT_CHECK, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 11bbedd80b..6af5be45d4 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -4,7 +4,7 @@ from esphome import core from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_SIZE +from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE from esphome.core import CORE, HexInt DEPENDENCIES = ["display"] @@ -61,8 +61,7 @@ def validate_pillow_installed(value): def validate_truetype_file(value): if value.endswith(".zip"): # for Google Fonts downloads raise cv.Invalid( - "Please unzip the font archive '{}' first and then use the .ttf files " - "inside.".format(value) + f"Please unzip the font archive '{value}' first and then use the .ttf files inside." ) if not value.endswith(".ttf"): raise cv.Invalid( @@ -73,9 +72,8 @@ def validate_truetype_file(value): DEFAULT_GLYPHS = ( - ' !"%()+,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' + ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) -CONF_RAW_DATA_ID = "raw_data_id" CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( @@ -110,7 +108,7 @@ async def to_code(config): _, (offset_x, offset_y) = font.font.getsize(glyph) width, height = mask.size width8 = ((width + 7) // 8) * 8 - glyph_data = [0 for _ in range(height * width8 // 8)] # noqa: F812 + glyph_data = [0] * (height * width8 // 8) for y in range(height): for x in range(width): if not mask.getpixel((x, y)): @@ -131,7 +129,7 @@ async def to_code(config): ("a_char", glyph), ( "data", - cg.RawExpression(str(prog_arr) + " + " + str(glyph_args[glyph][0])), + cg.RawExpression(f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), ), ("offset_x", glyph_args[glyph][1]), ("offset_y", glyph_args[glyph][2]), diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 892f9cd726..9e58f672c7 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -110,7 +110,7 @@ void FujitsuGeneralClimate::transmit_state() { // Set temperature uint8_t temperature_clamped = - (uint8_t) roundf(clamp(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX)); + (uint8_t) roundf(clamp(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX)); uint8_t temperature_offset = temperature_clamped - FUJITSU_GENERAL_TEMP_MIN; SET_NIBBLE(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset); @@ -133,7 +133,7 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_MODE_FAN_ONLY: SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN); break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: default: SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO); break; @@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { } } - // Validate footer - if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - for (uint8_t byte = 0; byte < recv_message_length; ++byte) { ESP_LOGVV(TAG, "%02X", recv_message[byte]); } @@ -344,7 +338,7 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { case FUJITSU_GENERAL_MODE_AUTO: default: // TODO: CLIMATE_MODE_10C is missing from esphome - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 7a26cd7b6b..8dc7a3e484 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -62,7 +62,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { /// Transmit via IR power off command. void transmit_off_(); - /// Parse incomming message + /// Parse incoming message bool on_receive(remote_base::RemoteReceiveData data) override; /// Transmit message as IR pulses diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 9039d0d62e..97a7ba3d54 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -14,6 +14,7 @@ from esphome.core import coroutine_with_priority CODEOWNERS = ["@esphome/core"] globals_ns = cg.esphome_ns.namespace("globals") GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) +RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component) GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action) MULTI_CONF = True @@ -32,22 +33,25 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) template_args = cg.TemplateArguments(type_) - res_type = GlobalsComponent.template(template_args) + restore = config[CONF_RESTORE_VALUE] + + 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]) - rhs = GlobalsComponent.new(template_args, initial_value) + rhs = type.new(template_args, initial_value) glob = cg.Pvariable(config[CONF_ID], rhs, res_type) await cg.register_component(glob, config) - if config[CONF_RESTORE_VALUE]: + if restore: value = config[CONF_ID].id if isinstance(value, str): value = value.encode() hash_ = int(hashlib.md5(value).hexdigest()[:8], 16) - cg.add(glob.set_restore_value(hash_)) + cg.add(glob.set_name_hash(hash_)) @automation.register_action( diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 397c55f6c4..3286e43575 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include namespace esphome { namespace globals { @@ -16,37 +17,46 @@ template class GlobalsComponent : public Component { memcpy(this->value_, initial_value.data(), sizeof(T)); } + T &value() { return this->value_; } + void setup() override {} + + protected: + T value_{}; +}; + +template class RestoringGlobalsComponent : public Component { + public: + using value_type = T; + explicit RestoringGlobalsComponent() = default; + explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {} + explicit RestoringGlobalsComponent( + std::array::type, std::extent::value> initial_value) { + memcpy(this->value_, initial_value.data(), sizeof(T)); + } + T &value() { return this->value_; } void setup() override { - if (this->restore_value_) { - this->rtc_ = global_preferences.make_preference(1944399030U ^ this->name_hash_); - this->rtc_.load(&this->value_); - } + this->rtc_ = global_preferences->make_preference(1944399030U ^ this->name_hash_); + this->rtc_.load(&this->value_); memcpy(&this->prev_value_, &this->value_, sizeof(T)); } float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override { - if (this->restore_value_) { - int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T)); - if (diff != 0) { - this->rtc_.save(&this->value_); - memcpy(&this->prev_value_, &this->value_, sizeof(T)); - } + int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T)); + if (diff != 0) { + this->rtc_.save(&this->value_); + memcpy(&this->prev_value_, &this->value_, sizeof(T)); } } - void set_restore_value(uint32_t name_hash) { - this->restore_value_ = true; - this->name_hash_ = name_hash; - } + void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } protected: T value_{}; T prev_value_{}; - bool restore_value_{false}; uint32_t name_hash_{}; ESPPreferenceObject rtc_; }; @@ -66,6 +76,7 @@ template class GlobalVarSetAction : public Action T &id(GlobalsComponent *value) { return value->value(); } +template T &id(RestoringGlobalsComponent *value) { return value->value(); } } // namespace globals } // namespace esphome diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index cfe49b3c94..33a173fe2e 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/gpio/output/gpio_binary_output.h b/esphome/components/gpio/output/gpio_binary_output.h index 0a7dfb46e2..6b72c61c0f 100644 --- a/esphome/components/gpio/output/gpio_binary_output.h +++ b/esphome/components/gpio/output/gpio_binary_output.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/binary_output.h" namespace esphome { diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 410a3818d6..56e0087eae 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -47,28 +47,28 @@ void GPIOSwitch::setup() { void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); LOG_PIN(" Pin: ", this->pin_); - const char *restore_mode = ""; + const LogString *restore_mode = LOG_STR(""); switch (this->restore_mode_) { case GPIO_SWITCH_RESTORE_DEFAULT_OFF: - restore_mode = "Restore (Defaults to OFF)"; + restore_mode = LOG_STR("Restore (Defaults to OFF)"); break; case GPIO_SWITCH_RESTORE_DEFAULT_ON: - restore_mode = "Restore (Defaults to ON)"; + restore_mode = LOG_STR("Restore (Defaults to ON)"); break; case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON: - restore_mode = "Restore inverted (Defaults to ON)"; + restore_mode = LOG_STR("Restore inverted (Defaults to ON)"); break; case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: - restore_mode = "Restore inverted (Defaults to OFF)"; + restore_mode = LOG_STR("Restore inverted (Defaults to OFF)"); break; case GPIO_SWITCH_ALWAYS_OFF: - restore_mode = "Always OFF"; + restore_mode = LOG_STR("Always OFF"); break; case GPIO_SWITCH_ALWAYS_ON: - restore_mode = "Always ON"; + restore_mode = LOG_STR("Always ON"); break; } - ESP_LOGCONFIG(TAG, " Restore Mode: %s", restore_mode); + ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode)); if (!this->interlock_.empty()) { ESP_LOGCONFIG(TAG, " Interlocks:"); for (auto *lock : this->interlock_) { diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index c0036b20e9..99f8060efa 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/switch/switch.h" namespace esphome { diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index de3eae1115..e485373175 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -15,9 +15,6 @@ from esphome.const import ( UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["uart"] @@ -31,37 +28,46 @@ GPSListener = gps_ns.class_("GPSListener") CONF_GPS_ID = "gps_id" MULTI_CONF = True -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(GPS), cv.Optional(CONF_LATITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_EMPTY, - 6, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=2, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( - UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_METER, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) .extend(cv.polling_component_schema("20s")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + cv.only_with_arduino, ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) async def to_code(config): @@ -94,8 +100,4 @@ async def to_code(config): cg.add(var.set_satellites_sensor(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus - cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict - - -def validate(config, item_config): - uart.validate_device("gps", config, item_config, require_tx=False) + cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 1e8ca94e9e..8c924d629c 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "gps.h" #include "esphome/core/log.h" @@ -69,3 +71,5 @@ void GPS::loop() { } // namespace gps } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 50dd476ae3..40cda145ca 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" @@ -63,3 +65,5 @@ class GPS : public PollingComponent, public uart::UARTDevice { } // namespace gps } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index 5352c7e059..e46f24ba8e 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "gps_time.h" #include "esphome/core/log.h" @@ -32,3 +34,5 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { } // namespace gps } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index a1f69a7130..d0d1db83b5 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" #include "esphome/components/gps/gps.h" @@ -22,3 +24,5 @@ class GPSTime : public time::RealTimeClock, public GPSListener { } // namespace gps } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py new file mode 100644 index 0000000000..12acfee869 --- /dev/null +++ b/esphome/components/graph/__init__.py @@ -0,0 +1,216 @@ +from esphome.components.font import Font +from esphome.components import sensor, color +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_COLOR, + CONF_DIRECTION, + CONF_DURATION, + CONF_ID, + CONF_LEGEND, + CONF_NAME, + CONF_NAME_FONT, + CONF_SHOW_LINES, + CONF_SHOW_UNITS, + CONF_SHOW_VALUES, + CONF_VALUE_FONT, + CONF_WIDTH, + CONF_SENSOR, + CONF_HEIGHT, + CONF_MIN_VALUE, + CONF_MAX_VALUE, + CONF_MIN_RANGE, + CONF_MAX_RANGE, + CONF_LINE_THICKNESS, + CONF_LINE_TYPE, + CONF_X_GRID, + CONF_Y_GRID, + CONF_BORDER, + CONF_TRACES, +) + +CODEOWNERS = ["@synco"] + +DEPENDENCIES = ["display", "sensor"] +MULTI_CONF = True + +graph_ns = cg.esphome_ns.namespace("graph") +Graph_ = graph_ns.class_("Graph", cg.Component) +GraphTrace = graph_ns.class_("GraphTrace") +GraphLegend = graph_ns.class_("GraphLegend") + +LineType = graph_ns.enum("LineType") +LINE_TYPE = { + "SOLID": LineType.LINE_TYPE_SOLID, + "DOTTED": LineType.LINE_TYPE_DOTTED, + "DASHED": LineType.LINE_TYPE_DASHED, +} + +DirectionType = graph_ns.enum("DirectionType") +DIRECTION_TYPE = { + "AUTO": DirectionType.DIRECTION_TYPE_AUTO, + "HORIZONTAL": DirectionType.DIRECTION_TYPE_HORIZONTAL, + "VERTICAL": DirectionType.DIRECTION_TYPE_VERTICAL, +} + +ValuePositionType = graph_ns.enum("ValuePositionType") +VALUE_POSITION_TYPE = { + "NONE": ValuePositionType.VALUE_POSITION_TYPE_NONE, + "AUTO": ValuePositionType.VALUE_POSITION_TYPE_AUTO, + "BESIDE": ValuePositionType.VALUE_POSITION_TYPE_BESIDE, + "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW, +} + + +GRAPH_TRACE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GraphTrace), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_NAME): cv.string, + 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), + } +) + +GRAPH_LEGEND_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(GraphLegend), + cv.Required(CONF_NAME_FONT): cv.use_id(Font), + cv.Optional(CONF_VALUE_FONT): cv.use_id(Font), + cv.Optional(CONF_WIDTH): cv.positive_not_null_int, + cv.Optional(CONF_HEIGHT): cv.positive_not_null_int, + cv.Optional(CONF_BORDER): cv.boolean, + cv.Optional(CONF_SHOW_LINES): cv.boolean, + cv.Optional(CONF_SHOW_VALUES): cv.enum(VALUE_POSITION_TYPE, upper=True), + cv.Optional(CONF_SHOW_UNITS): cv.boolean, + cv.Optional(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), + } +) + + +GRAPH_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(Graph_), + cv.Required(CONF_DURATION): cv.positive_time_period_seconds, + cv.Required(CONF_WIDTH): cv.positive_not_null_int, + cv.Required(CONF_HEIGHT): cv.positive_not_null_int, + cv.Optional(CONF_X_GRID): cv.positive_time_period_seconds, + cv.Optional(CONF_Y_GRID): cv.float_range(min=0, min_included=False), + cv.Optional(CONF_BORDER): cv.boolean, + # Single trace options in base + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + 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), + # Axis specific options (Future feature may be to add second Y-axis) + cv.Optional(CONF_MIN_VALUE): cv.float_, + cv.Optional(CONF_MAX_VALUE): cv.float_, + cv.Optional(CONF_MIN_RANGE): cv.float_range(min=0, min_included=False), + cv.Optional(CONF_MAX_RANGE): cv.float_range(min=0, min_included=False), + cv.Optional(CONF_TRACES): cv.ensure_list(GRAPH_TRACE_SCHEMA), + cv.Optional(CONF_LEGEND): cv.ensure_list(GRAPH_LEGEND_SCHEMA), + } +) + + +def _relocate_fields_to_subfolder(config, subfolder, subschema): + fields = [k.schema for k in subschema.schema.keys()] + fields.remove(CONF_ID) + if subfolder in config: + # Ensure no ambigious fields in base of config + for f in fields: + if f in config: + raise cv.Invalid( + "You cannot use the '" + + str(f) + + "' field when already using 'traces:'. " + "Please move it into 'traces:' entry." + ) + else: + # Copy over all fields to subfolder: + trace = {} + for f in fields: + if f in config: + trace[f] = config.pop(f) + config[subfolder] = cv.ensure_list(subschema)(trace) + return config + + +def _relocate_trace(config): + return _relocate_fields_to_subfolder(config, CONF_TRACES, GRAPH_TRACE_SCHEMA) + + +CONFIG_SCHEMA = cv.All( + GRAPH_SCHEMA, + _relocate_trace, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_duration(config[CONF_DURATION])) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + await cg.register_component(var, config) + + # Graph options + if CONF_X_GRID in config: + cg.add(var.set_grid_x(config[CONF_X_GRID])) + if CONF_Y_GRID in config: + cg.add(var.set_grid_y(config[CONF_Y_GRID])) + if CONF_BORDER in config: + cg.add(var.set_border(config[CONF_BORDER])) + # Axis related options + if CONF_MIN_VALUE in config: + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + if CONF_MAX_VALUE in config: + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + if CONF_MIN_RANGE in config: + cg.add(var.set_min_range(config[CONF_MIN_RANGE])) + if CONF_MAX_RANGE in config: + cg.add(var.set_max_range(config[CONF_MAX_RANGE])) + # Trace options + for trace in config[CONF_TRACES]: + tr = cg.new_Pvariable(trace[CONF_ID], GraphTrace()) + sens = await cg.get_variable(trace[CONF_SENSOR]) + cg.add(tr.set_sensor(sens)) + if CONF_NAME in trace: + cg.add(tr.set_name(trace[CONF_NAME])) + else: + cg.add(tr.set_name(trace[CONF_SENSOR].id)) + if CONF_LINE_THICKNESS in trace: + cg.add(tr.set_line_thickness(trace[CONF_LINE_THICKNESS])) + if CONF_LINE_TYPE in trace: + cg.add(tr.set_line_type(trace[CONF_LINE_TYPE])) + if CONF_COLOR in trace: + c = await cg.get_variable(trace[CONF_COLOR]) + cg.add(tr.set_line_color(c)) + cg.add(var.add_trace(tr)) + # Add legend + if CONF_LEGEND in config: + lgd = config[CONF_LEGEND][0] + legend = cg.new_Pvariable(lgd[CONF_ID], GraphLegend()) + if CONF_NAME_FONT in lgd: + font = await cg.get_variable(lgd[CONF_NAME_FONT]) + cg.add(legend.set_name_font(font)) + if CONF_VALUE_FONT in lgd: + font = await cg.get_variable(lgd[CONF_VALUE_FONT]) + cg.add(legend.set_value_font(font)) + if CONF_WIDTH in lgd: + cg.add(legend.set_width(lgd[CONF_WIDTH])) + if CONF_HEIGHT in lgd: + cg.add(legend.set_height(lgd[CONF_HEIGHT])) + if CONF_BORDER in lgd: + cg.add(legend.set_border(lgd[CONF_BORDER])) + if CONF_SHOW_LINES in lgd: + cg.add(legend.set_lines(lgd[CONF_SHOW_LINES])) + if CONF_SHOW_VALUES in lgd: + cg.add(legend.set_values(lgd[CONF_SHOW_VALUES])) + if CONF_SHOW_UNITS in lgd: + cg.add(legend.set_units(lgd[CONF_SHOW_UNITS])) + if CONF_DIRECTION in lgd: + cg.add(legend.set_direction(lgd[CONF_DIRECTION])) + cg.add(var.add_legend(legend)) + + cg.add_define("USE_GRAPH") diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp new file mode 100644 index 0000000000..a9daad4ab9 --- /dev/null +++ b/esphome/components/graph/graph.cpp @@ -0,0 +1,362 @@ +#include "graph.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/color.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include +#include +#include // std::cout, std::fixed +#include +namespace esphome { +namespace graph { + +using namespace display; + +static const char *const TAG = "graph"; +static const char *const TAGL = "graphlegend"; + +void HistoryData::init(int length) { + this->length_ = length; + this->samples_.resize(length, NAN); + this->last_sample_ = millis(); +} + +void HistoryData::take_sample(float data) { + uint32_t tm = millis(); + uint32_t dt = tm - last_sample_; + last_sample_ = tm; + + // Step data based on time + this->period_ += dt; + while (this->period_ >= this->update_time_) { + this->samples_[this->count_] = data; + this->period_ -= this->update_time_; + this->count_ = (this->count_ + 1) % this->length_; + ESP_LOGV(TAG, "Updating trace with value: %f", data); + } + if (!std::isnan(data)) { + // Recalc recent max/min + this->recent_min_ = data; + this->recent_max_ = data; + for (int i = 0; i < this->length_; i++) { + if (!std::isnan(this->samples_[i])) { + if (this->recent_max_ < this->samples_[i]) + this->recent_max_ = this->samples_[i]; + if (this->recent_min_ > this->samples_[i]) + this->recent_min_ = this->samples_[i]; + } + } + } +} + +void GraphTrace::init(Graph *g) { + ESP_LOGI(TAG, "Init trace for sensor %s", this->get_name().c_str()); + this->data_.init(g->get_width()); + sensor_->add_on_state_callback([this](float state) { this->data_.take_sample(state); }); + 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) { + /// Plot border + if (this->border_) { + buff->horizontal_line(x_offset, y_offset, this->width_, color); + buff->horizontal_line(x_offset, y_offset + this->height_ - 1, this->width_, color); + buff->vertical_line(x_offset, y_offset, this->height_, color); + buff->vertical_line(x_offset + this->width_ - 1, y_offset, this->height_, color); + } + /// Determine best y-axis scale and range + float ymin = NAN; + float ymax = NAN; + for (auto *trace : traces_) { + float mx = trace->get_tracedata()->get_recent_max(); + float mn = trace->get_tracedata()->get_recent_min(); + if (std::isnan(ymax) || (ymax < mx)) + ymax = mx; + if (std::isnan(ymin) || (ymin > mn)) + ymin = mn; + } + // Adjust if manually overridden + if (!std::isnan(this->min_value_)) + ymin = this->min_value_; + if (!std::isnan(this->max_value_)) + ymax = this->max_value_; + + float yrange = ymax - ymin; + if (yrange > this->max_range_) { + // Look back in trace data to best-fit into local range + float mx = NAN; + float mn = NAN; + for (int16_t i = 0; i < this->width_; i++) { + for (auto *trace : traces_) { + float v = trace->get_tracedata()->get_value(i); + if (!std::isnan(v)) { + if ((v - mn) > this->max_range_) + break; + if ((mx - v) > this->max_range_) + break; + if (std::isnan(mx) || (v > mx)) + mx = v; + if (std::isnan(mn) || (v < mn)) + mn = v; + } + } + } + yrange = this->max_range_; + if (!std::isnan(mn)) { + ymin = mn; + ymax = ymin + this->max_range_; + } + ESP_LOGV(TAG, "Graphing at max_range. Using local min %f, max %f", mn, mx); + } + + float y_per_div = this->min_range_; + if (!std::isnan(this->gridspacing_y_)) { + y_per_div = this->gridspacing_y_; + } + // Restrict drawing too many gridlines + if (yrange > 10 * y_per_div) { + while (yrange > 10 * y_per_div) { + y_per_div *= 2; + } + ESP_LOGW(TAG, "Graphing reducing y-scale to prevent too many gridlines"); + } + + // Adjust limits to nice y_per_div boundaries + int yn = int(ymin / y_per_div); + int ym = int(ymax / y_per_div) + int(1 * (fmodf(ymax, y_per_div) != 0)); + ymin = yn * y_per_div; + ymax = ym * y_per_div; + yrange = ymax - ymin; + + /// Draw grid + if (!std::isnan(this->gridspacing_y_)) { + for (int y = yn; y <= ym; y++) { + int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn))); + for (int x = 0; x < this->width_; x += 2) { + buff->draw_pixel_at(x_offset + x, y_offset + py, color); + } + } + } + if (!std::isnan(this->gridspacing_x_) && (this->gridspacing_x_ > 0)) { + int n = this->duration_ / this->gridspacing_x_; + // Restrict drawing too many gridlines + if (n > 20) { + while (n > 20) { + n /= 2; + } + ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines"); + } + for (int i = 0; i <= n; i++) { + for (int y = 0; y < this->height_; y += 2) { + buff->draw_pixel_at(x_offset + i * (this->width_ - 1) / n, y_offset + y, color); + } + } + } + + /// Draw traces + 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(); + for (int16_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 (int16_t t = 0; t < thick; t++) { + buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); + } + } + } + } + } +} + +/// Determine the best coordinates of drawing text + lines +void GraphLegend::init(Graph *g) { + parent_ = g; + + // Determine maximum expected text and value width / height + int txtw = 0, txtos = 0, txtbl = 0, txth = 0; + int valw = 0, valos = 0, valbl = 0, valh = 0; + int lt = 0; + for (auto *trace : g->traces_) { + std::string txtstr = trace->get_name(); + int fw, fos, fbl, fh; + this->font_label_->measure(txtstr.c_str(), &fw, &fos, &fbl, &fh); + if (fw > txtw) + txtw = fw; + if (fh > txth) + txth = fh; + if (trace->get_line_thickness() > lt) + lt = trace->get_line_thickness(); + ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh); + + if (this->values_ != VALUE_POSITION_TYPE_NONE) { + std::stringstream ss; + ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); + std::string valstr = ss.str(); + if (this->units_) { + valstr += trace->sensor_->get_unit_of_measurement(); + } + this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); + if (fw > valw) + valw = fw; + if (fh > valh) + valh = fh; + ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh); + } + } + // Add extra margin + txtw *= 1.2; + valw *= 1.2; + + uint8_t n = g->traces_.size(); + uint16_t w = this->width_; + uint16_t h = this->height_; + DirectionType dir = this->direction_; + ValuePositionType valpos = this->values_; + if (!this->font_value_) { + valpos = VALUE_POSITION_TYPE_NONE; + } + // Line sample always goes below text for compactness + this->yl_ = txth + (txth / 4) + lt / 2; + + if (dir == DIRECTION_TYPE_AUTO) { + dir = DIRECTION_TYPE_HORIZONTAL; // as default + if (h > 0) { + dir = DIRECTION_TYPE_VERTICAL; + } + } + + if (valpos == VALUE_POSITION_TYPE_AUTO) { + // TODO: do something smarter?? - fit to w and h? + valpos = VALUE_POSITION_TYPE_BELOW; + } + + if (valpos == VALUE_POSITION_TYPE_BELOW) { + this->yv_ = txth + (txth / 4); + if (this->lines_) + this->yv_ += txth / 4 + lt; + } else if (valpos == VALUE_POSITION_TYPE_BESIDE) { + this->xv_ = (txtw + valw) / 2; + } + + // If width or height is specified we divide evenly within, else we do tight-fit + if (w == 0) { + this->x0_ = txtw / 2; + this->xs_ = txtw; + if (valpos == VALUE_POSITION_TYPE_BELOW) { + this->xs_ = std::max(txtw, valw); + ; + this->x0_ = this->xs_ / 2; + } else if (valpos == VALUE_POSITION_TYPE_BESIDE) { + this->xs_ = txtw + valw; + } + if (dir == DIRECTION_TYPE_VERTICAL) { + this->width_ = this->xs_; + } else { + this->width_ = this->xs_ * n; + } + } else { + this->xs_ = w / n; + this->x0_ = this->xs_ / 2; + } + + if (h == 0) { + this->ys_ = txth; + if (valpos == VALUE_POSITION_TYPE_BELOW) { + this->ys_ = txth + txth / 2 + valh; + if (this->lines_) { + this->ys_ += lt; + } + } else if (valpos == VALUE_POSITION_TYPE_BESIDE) { + if (this->lines_) { + this->ys_ = std::max(txth + txth / 4 + lt + txth / 4, valh + valh / 4); + } else { + this->ys_ = std::max(txth + txth / 4, valh + valh / 4); + } + this->height_ = this->ys_ * n; + } + if (dir == DIRECTION_TYPE_HORIZONTAL) { + this->height_ = this->ys_; + } else { + this->height_ = this->ys_ * n; + } + } else { + this->ys_ = h / n; + } + + if (dir == DIRECTION_TYPE_HORIZONTAL) { + this->ys_ = 0; + } else { + this->xs_ = 0; + } +} + +void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { + if (!legend_) + return; + + /// Plot border + if (this->border_) { + int w = legend_->width_; + int h = legend_->height_; + buff->horizontal_line(x_offset, y_offset, w, color); + buff->horizontal_line(x_offset, y_offset + h - 1, w, color); + buff->vertical_line(x_offset, y_offset, h, color); + buff->vertical_line(x_offset + w - 1, y_offset, h, color); + } + + int x = x_offset + legend_->x0_; + int y = y_offset; + for (auto *trace : traces_) { + std::string txtstr = trace->get_name(); + ESP_LOGV(TAG, " %s", txtstr.c_str()); + + buff->printf(x, y, legend_->font_label_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", txtstr.c_str()); + + if (legend_->lines_) { + uint16_t thick = trace->get_line_thickness(); + for (int16_t i = 0; i < legend_->x0_ * 4 / 3; i++) { + uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick; + if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) { + buff->vertical_line(x - legend_->x0_ * 2 / 3 + i, y + legend_->yl_ - thick / 2, thick, + trace->get_line_color()); + } + } + } + + if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { + int xv = x + legend_->xv_; + int yv = y + legend_->yv_; + std::stringstream ss; + ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); + std::string valstr = ss.str(); + if (legend_->units_) { + valstr += trace->sensor_->get_unit_of_measurement(); + } + buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); + ESP_LOGV(TAG, " value: %s", valstr.c_str()); + } + x += legend_->xs_; + y += legend_->ys_; + } +} + +void Graph::setup() { + for (auto *trace : traces_) { + trace->init(this); + } +} + +void Graph::dump_config() { + for (auto *trace : traces_) { + ESP_LOGCONFIG(TAG, "Graph for sensor %s", trace->get_name().c_str()); + } +} + +} // namespace graph +} // namespace esphome diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h new file mode 100644 index 0000000000..f935917c57 --- /dev/null +++ b/esphome/components/graph/graph.h @@ -0,0 +1,179 @@ +#pragma once +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include +#include + +namespace esphome { + +// forward declare DisplayBuffer +namespace display { +class DisplayBuffer; +class Font; +} // namespace display + +namespace graph { + +class Graph; + +const Color COLOR_ON(255, 255, 255, 255); + +/// Bit pattern defines the line-type +enum LineType { + LINE_TYPE_SOLID = 0b1111, + LINE_TYPE_DOTTED = 0b0101, + LINE_TYPE_DASHED = 0b1110, + // Following defines number of bits used to define line pattern + PATTERN_LENGTH = 4 +}; + +enum DirectionType { + DIRECTION_TYPE_AUTO, + DIRECTION_TYPE_HORIZONTAL, + DIRECTION_TYPE_VERTICAL, +}; + +enum ValuePositionType { + VALUE_POSITION_TYPE_NONE, + VALUE_POSITION_TYPE_AUTO, + VALUE_POSITION_TYPE_BESIDE, + VALUE_POSITION_TYPE_BELOW +}; + +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_width(uint32_t width) { this->width_ = width; } + void set_height(uint32_t height) { this->height_ = height; } + void set_border(bool val) { this->border_ = val; } + void set_lines(bool val) { this->lines_ = val; } + void set_values(ValuePositionType val) { this->values_ = val; } + void set_units(bool val) { this->units_ = val; } + void set_direction(DirectionType val) { this->direction_ = val; } + + protected: + uint32_t width_{0}; + uint32_t height_{0}; + bool border_{true}; + bool lines_{true}; + ValuePositionType values_{VALUE_POSITION_TYPE_AUTO}; + bool units_{true}; + DirectionType direction_{DIRECTION_TYPE_AUTO}; + display::Font *font_label_{nullptr}; + display::Font *font_value_{nullptr}; + // Calculated values + Graph *parent_{nullptr}; + // (x0) (xs,ys) (xs,ys) + // ------> LABEL1 -------> LABEL2 -------> ... + // | \(xv,yv) \ . + // | \ \-> VALUE1+units + // (0,yl)| \-> VALUE1+units + // v (top_center) + // LINE_SAMPLE + int x0_{0}; // X-offset to centre of label text + int xs_{0}; // X spacing between labels + int ys_{0}; // Y spacing between labels + int yl_{0}; // Y spacing from label to line sample + int xv_{0}; // X distance between label to value text + int yv_{0}; // Y distance between label to value text + friend Graph; +}; + +class HistoryData { + public: + void init(int length); + ~HistoryData(); + void set_update_time_ms(uint32_t update_time_ms) { update_time_ = update_time_ms; } + void take_sample(float data); + int get_length() const { return length_; } + float get_value(int idx) const { return samples_[(count_ + length_ - 1 - idx) % length_]; } + float get_recent_max() const { return recent_max_; } + float get_recent_min() const { return recent_min_; } + + protected: + uint32_t last_sample_; + uint32_t period_{0}; /// in ms + uint32_t update_time_{0}; /// in ms + int length_; + int count_{0}; + float recent_min_{NAN}; + float recent_max_{NAN}; + std::vector samples_; +}; + +class GraphTrace { + public: + void init(Graph *g); + void set_name(std::string name) { name_ = std::move(name); } + void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + uint8_t get_line_thickness() { return this->line_thickness_; } + void set_line_thickness(uint8_t val) { this->line_thickness_ = val; } + enum LineType get_line_type() { return this->line_type_; } + 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; } + const std::string get_name() { return name_; } + const HistoryData *get_tracedata() { return &data_; } + + protected: + sensor::Sensor *sensor_{nullptr}; + std::string name_{""}; + uint8_t line_thickness_{3}; + enum LineType line_type_ { LINE_TYPE_SOLID }; + Color line_color_{COLOR_ON}; + HistoryData data_; + + friend Graph; + friend GraphLegend; +}; + +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 setup() override; + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void dump_config() override; + + void set_duration(uint32_t duration) { duration_ = duration; } + void set_width(uint32_t width) { width_ = width; } + void set_height(uint32_t height) { height_ = height; } + void set_min_value(float val) { this->min_value_ = val; } + void set_max_value(float val) { this->max_value_ = val; } + void set_min_range(float val) { this->min_range_ = val; } + void set_max_range(float val) { this->max_range_ = val; } + void set_grid_x(float val) { this->gridspacing_x_ = val; } + void set_grid_y(float val) { this->gridspacing_y_ = val; } + void set_border(bool val) { this->border_ = val; } + void add_trace(GraphTrace *trace) { traces_.push_back(trace); } + void add_legend(GraphLegend *legend) { + this->legend_ = legend; + legend->init(this); + } + uint32_t get_duration() { return duration_; } + uint32_t get_width() { return width_; } + uint32_t get_height() { return height_; } + + protected: + uint32_t duration_; /// in seconds + uint32_t width_; /// in pixels + uint32_t height_; /// in pixels + float min_value_{NAN}; + float max_value_{NAN}; + float min_range_{1.0}; + float max_range_{NAN}; + float gridspacing_x_{NAN}; + float gridspacing_y_{NAN}; + bool border_{true}; + std::vector traces_; + GraphLegend *legend_{nullptr}; + + friend GraphLegend; +}; + +} // namespace graph +} // namespace esphome diff --git a/esphome/components/havells_solar/__init__.py b/esphome/components/havells_solar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp new file mode 100644 index 0000000000..f029df10ad --- /dev/null +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -0,0 +1,165 @@ +#include "havells_solar.h" +#include "havells_solar_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace havells_solar { + +static const char *const TAG = "havells_solar"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x03; +static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers + +void HavellsSolar::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for HavellsSolar!"); + return; + } + + /* Usage: returns the float value of 1 register read by modbus + Arg1: Register address * number of bytes per register + Arg2: Multiplier for final register value + */ + auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); + return temp * unit; + }; + + /* Usage: returns the float value of 2 registers read by modbus + Arg1: Register address * number of bytes per register + Arg2: Multiplier for final register value + */ + auto havells_solar_get_1_register = [&](size_t i, float unit) -> float { + uint16_t temp = encode_uint16(data[i], data[i + 1]); + return temp * unit; + }; + + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + + float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); + float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); + + if (phase.voltage_sensor_ != nullptr) + phase.voltage_sensor_->publish_state(voltage); + if (phase.current_sensor_ != nullptr) + phase.current_sensor_->publish_state(current); + } + + for (uint8_t i = 0; i < 2; i++) { + auto pv = this->pvs_[i]; + if (!pv.setup) + continue; + + float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); + float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); + float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT); + float voltage_sampled_by_secondary_cpu = + havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT); + float insulation_of_p_to_ground = + havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT); + + if (pv.voltage_sensor_ != nullptr) + pv.voltage_sensor_->publish_state(voltage); + if (pv.current_sensor_ != nullptr) + pv.current_sensor_->publish_state(current); + if (pv.active_power_sensor_ != nullptr) + pv.active_power_sensor_->publish_state(active_power); + if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr) + pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu); + if (pv.insulation_of_p_to_ground_sensor_ != nullptr) + pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground); + } + + float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT); + float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT); + float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT); + float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT); + float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT); + float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT); + float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT); + float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT); + float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT); + float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT); + float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT); + float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT); + float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT); + float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT); + float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT); + + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->today_production_sensor_ != nullptr) + this->today_production_sensor_->publish_state(today_production); + if (this->total_energy_production_sensor_ != nullptr) + this->total_energy_production_sensor_->publish_state(total_energy_production); + if (this->total_generation_time_sensor_ != nullptr) + this->total_generation_time_sensor_->publish_state(total_generation_time); + if (this->today_generation_time_sensor_ != nullptr) + this->today_generation_time_sensor_->publish_state(today_generation_time); + if (this->inverter_module_temp_sensor_ != nullptr) + this->inverter_module_temp_sensor_->publish_state(inverter_module_temp); + if (this->inverter_inner_temp_sensor_ != nullptr) + this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp); + if (this->inverter_bus_voltage_sensor_ != nullptr) + this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage); + if (this->insulation_pv_n_to_ground_sensor_ != nullptr) + this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground); + if (this->gfci_value_sensor_ != nullptr) + this->gfci_value_sensor_->publish_state(gfci_value); + if (this->dci_of_r_sensor_ != nullptr) + this->dci_of_r_sensor_->publish_state(dci_of_r); + if (this->dci_of_s_sensor_ != nullptr) + this->dci_of_s_sensor_->publish_state(dci_of_s); + if (this->dci_of_t_sensor_ != nullptr) + this->dci_of_t_sensor_->publish_state(dci_of_t); +} + +void HavellsSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void HavellsSolar::dump_config() { + ESP_LOGCONFIG(TAG, "HAVELLS Solar:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + ESP_LOGCONFIG(TAG, " Phase %c", i + 'A'); + LOG_SENSOR(" ", "Voltage", phase.voltage_sensor_); + LOG_SENSOR(" ", "Current", phase.current_sensor_); + } + for (uint8_t i = 0; i < 2; i++) { + auto pv = this->pvs_[i]; + if (!pv.setup) + continue; + ESP_LOGCONFIG(TAG, " PV %d", i + 1); + LOG_SENSOR(" ", "Voltage", pv.voltage_sensor_); + LOG_SENSOR(" ", "Current", pv.current_sensor_); + LOG_SENSOR(" ", "Active Power", pv.active_power_sensor_); + LOG_SENSOR(" ", "Voltage Sampled By Secondary CPU", pv.voltage_sampled_by_secondary_cpu_sensor_); + LOG_SENSOR(" ", "Insulation Of PV+ To Ground", pv.insulation_of_p_to_ground_sensor_); + } + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Today Generation", this->today_production_sensor_); + LOG_SENSOR(" ", "Total Generation", this->total_energy_production_sensor_); + LOG_SENSOR(" ", "Total Generation Time", this->total_generation_time_sensor_); + LOG_SENSOR(" ", "Today Generation Time", this->today_generation_time_sensor_); + LOG_SENSOR(" ", "Inverter Module Temp", this->inverter_module_temp_sensor_); + LOG_SENSOR(" ", "Inverter Inner Temp", this->inverter_inner_temp_sensor_); + LOG_SENSOR(" ", "Inverter Bus Voltage", this->inverter_bus_voltage_sensor_); + LOG_SENSOR(" ", "Insulation Of PV- To Ground", this->insulation_pv_n_to_ground_sensor_); + LOG_SENSOR(" ", "GFCI Value", this->gfci_value_sensor_); + LOG_SENSOR(" ", "DCI Of R", this->dci_of_r_sensor_); + LOG_SENSOR(" ", "DCI Of S", this->dci_of_s_sensor_); + LOG_SENSOR(" ", "DCI Of T", this->dci_of_t_sensor_); +} + +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h new file mode 100644 index 0000000000..2ccc8be3d4 --- /dev/null +++ b/esphome/components/havells_solar/havells_solar.h @@ -0,0 +1,115 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace havells_solar { + +class HavellsSolar : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].current_sensor_ = current_sensor; + } + void set_voltage_sensor_pv(uint8_t pv, sensor::Sensor *voltage_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor_pv(uint8_t pv, sensor::Sensor *current_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].current_sensor_ = current_sensor; + } + void set_active_power_sensor_pv(uint8_t pv, sensor::Sensor *active_power_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].active_power_sensor_ = active_power_sensor; + } + void set_voltage_sampled_by_secondary_cpu_sensor_pv(uint8_t pv, + sensor::Sensor *voltage_sampled_by_secondary_cpu_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].voltage_sampled_by_secondary_cpu_sensor_ = voltage_sampled_by_secondary_cpu_sensor; + } + void set_insulation_of_p_to_ground_sensor_pv(uint8_t pv, sensor::Sensor *insulation_of_p_to_ground_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].insulation_of_p_to_ground_sensor_ = insulation_of_p_to_ground_sensor; + } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } + void set_active_power_sensor(sensor::Sensor *active_power_sensor) { + this->active_power_sensor_ = active_power_sensor; + } + void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) { + this->reactive_power_sensor_ = reactive_power_sensor; + } + void set_today_production_sensor(sensor::Sensor *today_production_sensor) { + this->today_production_sensor_ = today_production_sensor; + } + void set_total_energy_production_sensor(sensor::Sensor *total_energy_production_sensor) { + this->total_energy_production_sensor_ = total_energy_production_sensor; + } + void set_total_generation_time_sensor(sensor::Sensor *total_generation_time_sensor) { + this->total_generation_time_sensor_ = total_generation_time_sensor; + } + void set_today_generation_time_sensor(sensor::Sensor *today_generation_time_sensor) { + this->today_generation_time_sensor_ = today_generation_time_sensor; + } + void set_inverter_module_temp_sensor(sensor::Sensor *inverter_module_temp_sensor) { + this->inverter_module_temp_sensor_ = inverter_module_temp_sensor; + } + void set_inverter_inner_temp_sensor(sensor::Sensor *inverter_inner_temp_sensor) { + this->inverter_inner_temp_sensor_ = inverter_inner_temp_sensor; + } + void set_inverter_bus_voltage_sensor(sensor::Sensor *inverter_bus_voltage_sensor) { + this->inverter_bus_voltage_sensor_ = inverter_bus_voltage_sensor; + } + void set_insulation_pv_n_to_ground_sensor(sensor::Sensor *insulation_pv_n_to_ground_sensor) { + this->insulation_pv_n_to_ground_sensor_ = insulation_pv_n_to_ground_sensor; + } + void set_gfci_value_sensor(sensor::Sensor *gfci_value_sensor) { this->gfci_value_sensor_ = gfci_value_sensor; } + void set_dci_of_r_sensor(sensor::Sensor *dci_of_r_sensor) { this->dci_of_r_sensor_ = dci_of_r_sensor; } + void set_dci_of_s_sensor(sensor::Sensor *dci_of_s_sensor) { this->dci_of_s_sensor_ = dci_of_s_sensor; } + void set_dci_of_t_sensor(sensor::Sensor *dci_of_t_sensor) { this->dci_of_t_sensor_ = dci_of_t_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + struct HAVELLSPhase { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + } phases_[3]; + struct HAVELLSPV { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *voltage_sampled_by_secondary_cpu_sensor_{nullptr}; + sensor::Sensor *insulation_of_p_to_ground_sensor_{nullptr}; + } pvs_[2]; + sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *today_production_sensor_{nullptr}; + sensor::Sensor *total_energy_production_sensor_{nullptr}; + sensor::Sensor *total_generation_time_sensor_{nullptr}; + sensor::Sensor *today_generation_time_sensor_{nullptr}; + sensor::Sensor *inverter_module_temp_sensor_{nullptr}; + sensor::Sensor *inverter_inner_temp_sensor_{nullptr}; + sensor::Sensor *inverter_bus_voltage_sensor_{nullptr}; + sensor::Sensor *insulation_pv_n_to_ground_sensor_{nullptr}; + sensor::Sensor *gfci_value_sensor_{nullptr}; + sensor::Sensor *dci_of_r_sensor_{nullptr}; + sensor::Sensor *dci_of_s_sensor_{nullptr}; + sensor::Sensor *dci_of_t_sensor_{nullptr}; +}; + +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/havells_solar_registers.h b/esphome/components/havells_solar/havells_solar_registers.h new file mode 100644 index 0000000000..8e1cb3ec7a --- /dev/null +++ b/esphome/components/havells_solar/havells_solar_registers.h @@ -0,0 +1,49 @@ +#pragma once +namespace esphome { +namespace havells_solar { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; + +/* PV Input Message */ +static const uint16_t HAVELLS_PV_1_VOLTAGE = 0x0006; +static const uint16_t HAVELLS_PV_1_CURRENT = 0x0007; +static const uint16_t HAVELLS_PV_2_VOLTAGE = 0x0008; +static const uint16_t HAVELLS_PV_2_CURRENT = 0x0009; +static const uint16_t HAVELLS_PV_1_POWER = 0x000A; +static const uint16_t HAVELLS_PV_2_POWER = 0x000B; + +/* Output Grid Message */ +static const uint16_t HAVELLS_SYSTEM_ACTIVE_POWER = 0x000C; +static const uint16_t HAVELLS_SYSTEM_REACTIVE_POWER = 0x000D; +static const uint16_t HAVELLS_GRID_FREQUENCY = 0x000E; +static const uint16_t HAVELLS_PHASE_1_VOLTAGE = 0x000F; +static const uint16_t HAVELLS_PHASE_1_CURRENT = 0x0010; +static const uint16_t HAVELLS_PHASE_2_VOLTAGE = 0x0011; +static const uint16_t HAVELLS_PHASE_2_CURRENT = 0x0012; +static const uint16_t HAVELLS_PHASE_3_VOLTAGE = 0x0013; +static const uint16_t HAVELLS_PHASE_3_CURRENT = 0x0014; + +/* Inverter Generation message */ +static const uint16_t HAVELLS_TOTAL_ENERGY_PRODUCTION = 0x0015; +static const uint16_t HAVELLS_TOTAL_GENERATION_TIME = 0x0017; +static const uint16_t HAVELLS_TODAY_PRODUCTION = 0x0019; +static const uint16_t HAVELLS_TODAY_GENERATION_TIME = 0x001A; + +/* Inverter inner message */ +static const uint16_t HAVELLS_INVERTER_MODULE_TEMP = 0x001B; +static const uint16_t HAVELLS_INVERTER_INNER_TEMP = 0x001C; +static const uint16_t HAVELLS_INVERTER_BUS_VOLTAGE = 0x001D; +static const uint16_t HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = 0x001E; +static const uint16_t HAVELLS_PV2_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = 0x001F; +static const uint16_t HAVELLS_PV1_INSULATION_OF_P_TO_GROUND = 0x0024; +static const uint16_t HAVELLS_PV2_INSULATION_OF_P_TO_GROUND = 0x0025; +static const uint16_t HAVELLS_INSULATION_OF_PV_N_TO_GROUND = 0x0026; +static const uint16_t HAVELLS_GFCI_VALUE = 0x002A; +static const uint16_t HAVELLS_DCI_OF_R = 0x002B; +static const uint16_t HAVELLS_DCI_OF_S = 0x002C; +static const uint16_t HAVELLS_DCI_OF_T = 0x002D; +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py new file mode 100644 index 0000000000..3ec12d5b83 --- /dev/null +++ b/esphome/components/havells_solar/sensor.py @@ -0,0 +1,293 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_CURRENT, + CONF_FREQUENCY, + CONF_ID, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_DEGREES, + UNIT_HERTZ, + UNIT_MINUTE, + UNIT_VOLT, + UNIT_VOLT_AMPS_REACTIVE, + 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" +CONF_TODAY_GENERATION_TIME = "today_generation_time" +CONF_PV1 = "pv1" +CONF_PV2 = "pv2" +UNIT_KILOWATT_HOURS = "kWh" +UNIT_HOURS = "h" +UNIT_KOHM = "kΩ" +UNIT_MILLIAMPERE = "mA" + + +CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" +CONF_INVERTER_INNER_TEMP = "inverter_inner_temp" +CONF_INVERTER_BUS_VOLTAGE = "inverter_bus_voltage" +CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = "voltage_sampled_by_secondary_cpu" +CONF_INSULATION_OF_P_TO_GROUND = "insulation_of_p_to_ground" +CONF_INSULATION_OF_PV_N_TO_GROUND = "insulation_of_pv_n_to_ground" +CONF_GFCI_VALUE = "gfci_value" +CONF_DCI_OF_R = "dci_of_r" +CONF_DCI_OF_S = "dci_of_s" +CONF_DCI_OF_T = "dci_of_t" + + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +havells_solar_ns = cg.esphome_ns.namespace("havells_solar") +HavellsSolar = havells_solar_ns.class_( + "HavellsSolar", cg.PollingComponent, modbus.ModbusDevice +) + +PHASE_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), +} +PV_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), +} + +PHASE_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()} +) +PV_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HavellsSolar), + cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, + cv.Optional(CONF_PV1): PV_SCHEMA, + cv.Optional(CONF_PV2): PV_SCHEMA, + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + 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, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_HOURS, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INVERTER_INNER_TEMP): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_frequency_sensor(sens)) + + if CONF_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER]) + cg.add(var.set_active_power_sensor(sens)) + + if CONF_REACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER]) + cg.add(var.set_reactive_power_sensor(sens)) + + if CONF_ENERGY_PRODUCTION_DAY in config: + sens = await sensor.new_sensor(config[CONF_ENERGY_PRODUCTION_DAY]) + cg.add(var.set_today_production_sensor(sens)) + + if CONF_TOTAL_ENERGY_PRODUCTION in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_ENERGY_PRODUCTION]) + cg.add(var.set_total_energy_production_sensor(sens)) + + if CONF_TOTAL_GENERATION_TIME in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_GENERATION_TIME]) + cg.add(var.set_total_generation_time_sensor(sens)) + + if CONF_TODAY_GENERATION_TIME in config: + sens = await sensor.new_sensor(config[CONF_TODAY_GENERATION_TIME]) + cg.add(var.set_today_generation_time_sensor(sens)) + + if CONF_INVERTER_MODULE_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP]) + cg.add(var.set_inverter_module_temp_sensor(sens)) + + if CONF_INVERTER_INNER_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_INNER_TEMP]) + cg.add(var.set_inverter_inner_temp_sensor(sens)) + + if CONF_INVERTER_BUS_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_BUS_VOLTAGE]) + cg.add(var.set_inverter_bus_voltage_sensor(sens)) + + if CONF_INSULATION_OF_PV_N_TO_GROUND in config: + sens = await sensor.new_sensor(config[CONF_INSULATION_OF_PV_N_TO_GROUND]) + cg.add(var.set_insulation_pv_n_to_ground_sensor(sens)) + + if CONF_GFCI_VALUE in config: + sens = await sensor.new_sensor(config[CONF_GFCI_VALUE]) + cg.add(var.set_gfci_value_sensor(sens)) + + if CONF_DCI_OF_R in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_R]) + cg.add(var.set_dci_of_r_sensor(sens)) + + if CONF_DCI_OF_S in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_S]) + cg.add(var.set_dci_of_s_sensor(sens)) + + if CONF_DCI_OF_T in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_T]) + cg.add(var.set_dci_of_t_sensor(sens)) + + for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): + if phase not in config: + continue + + phase_config = config[phase] + for sensor_type in PHASE_SENSORS: + if sensor_type in phase_config: + sens = await sensor.new_sensor(phase_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor")(i, sens)) + + for i, pv in enumerate([CONF_PV1, CONF_PV2]): + if pv not in config: + continue + + pv_config = config[pv] + for sensor_type in pv_config: + if sensor_type in pv_config: + sens = await sensor.new_sensor(pv_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor_pv")(i, sens)) diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py index e69de29bb2..7eae863ff5 100644 --- a/esphome/components/hbridge/__init__.py +++ b/esphome/components/hbridge/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +hbridge_ns = cg.esphome_ns.namespace("hbridge") diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py new file mode 100644 index 0000000000..b169978acd --- /dev/null +++ b/esphome/components/hbridge/fan/__init__.py @@ -0,0 +1,70 @@ +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 fan, output +from esphome.const import ( + CONF_ID, + CONF_DECAY_MODE, + CONF_SPEED_COUNT, + CONF_PIN_A, + CONF_PIN_B, + CONF_ENABLE_PIN, +) +from .. import hbridge_ns + + +CODEOWNERS = ["@WeekendWarrior"] + + +HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState) + +DecayMode = hbridge_ns.enum("DecayMode") +DECAY_MODE_OPTIONS = { + "SLOW": DecayMode.DECAY_MODE_SLOW, + "FAST": DecayMode.DECAY_MODE_FAST, +} + +# Actions +BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "fan.hbridge.brake", + BrakeAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), +) +async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SPEED_COUNT], + config[CONF_DECAY_MODE], + ) + await fan.register_fan(var, config) + pin_a_ = await cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pin_a(pin_a_)) + pin_b_ = await cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pin_b(pin_b_)) + + if CONF_ENABLE_PIN in config: + enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) + cg.add(var.set_enable_pin(enable_pin)) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp new file mode 100644 index 0000000000..a4e5429ff4 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -0,0 +1,85 @@ +#include "hbridge_fan.h" +#include "esphome/components/fan/fan_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +static const char *const TAG = "fan.hbridge"; + +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level); +} + +// constant IN1/IN2, PWM on EN => power control, fast current decay +// constant IN1/EN, PWM on IN2 => power control, slow current decay +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + this->enable_->set_level(enable); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable); +} + +fan::FanStateCall HBridgeFan::brake() { + ESP_LOGD(TAG, "Braking"); + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f); + return this->make_call().set_state(false); +} + +void HBridgeFan::dump_config() { + ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str()); + if (this->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation: YES"); + } + if (this->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } + if (this->decay_mode_ == DECAY_MODE_SLOW) { + ESP_LOGCONFIG(TAG, " Decay Mode: Slow"); + } else { + ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); + } +} +void HBridgeFan::setup() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->set_traits(traits); + this->add_on_state_callback([this]() { this->next_update_ = true; }); +} +void HBridgeFan::loop() { + if (!this->next_update_) { + return; + } + this->next_update_ = false; + + float speed = 0.0f; + if (this->state) { + speed = static_cast(this->speed) / static_cast(this->speed_count_); + } + if (speed == 0.0f) { // off means idle + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed) + : this->set_hbridge_levels_(speed, speed, speed); + return; + } + if (this->direction == fan::FAN_DIRECTION_FORWARD) { + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f) + : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed) + : this->set_hbridge_levels_(0.0f, 1.0f, speed); + } + } else { // fan::FAN_DIRECTION_REVERSE + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed) + : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f) + : this->set_hbridge_levels_(1.0f, 0.0f, speed); + } + } +} + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h new file mode 100644 index 0000000000..984318c8d6 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace hbridge { + +enum DecayMode { + DECAY_MODE_SLOW = 0, + DECAY_MODE_FAST = 1, +}; + +class HBridgeFan : public fan::FanState { + public: + HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + + 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 setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + fan::FanStateCall brake(); + + int get_speed_count() { return this->speed_count_; } + // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping + void internal_update() { this->next_update_ = true; } + + protected: + output::FloatOutput *pin_a_; + output::FloatOutput *pin_b_; + output::FloatOutput *enable_{nullptr}; + output::BinaryOutput *oscillating_{nullptr}; + bool next_update_{true}; + int speed_count_{}; + DecayMode decay_mode_{DECAY_MODE_SLOW}; + + void set_hbridge_levels_(float a_level, float b_level); + void set_hbridge_levels_(float a_level, float b_level, float enable); +}; + +template class BrakeAction : public Action { + public: + explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->brake(); } + + HBridgeFan *parent_; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light/__init__.py similarity index 94% rename from esphome/components/hbridge/light.py rename to esphome/components/hbridge/light/__init__.py index b4ae45977a..fe5c3e9845 100644 --- a/esphome/components/hbridge/light.py +++ b/esphome/components/hbridge/light/__init__.py @@ -2,8 +2,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B +from .. import hbridge_ns + +CODEOWNERS = ["@DotNetDann"] -hbridge_ns = cg.esphome_ns.namespace("hbridge") HBridgeLightOutput = hbridge_ns.class_( "HBridgeLightOutput", cg.PollingComponent, light.LightOutput ) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h similarity index 65% rename from esphome/components/hbridge/hbridge_light_output.h rename to esphome/components/hbridge/light/hbridge_light_output.h index 03a5b3a88c..c309154852 100644 --- a/esphome/components/hbridge/hbridge_light_output.h +++ b/esphome/components/hbridge/light/hbridge_light_output.h @@ -18,10 +18,9 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); // Dimming - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(true); // hbridge color - traits.set_supports_color_temperature(false); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); return traits; } @@ -31,11 +30,11 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { // This method runs around 60 times per second // We cannot do the PWM ourselves so we are reliant on the hardware PWM if (!this->forward_direction_) { // First LED Direction - this->pinb_pin_->set_level(this->duty_off_); this->pina_pin_->set_level(this->pina_duty_); + this->pinb_pin_->set_level(0); this->forward_direction_ = true; } else { // Second LED Direction - this->pina_pin_->set_level(this->duty_off_); + this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; } @@ -44,23 +43,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - float bright; - state->current_values_as_brightness(&bright); - - state->set_gamma_correct(0); - float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); - - if ((white / bright) > 0.55) { - this->pina_duty_ = (bright * (1 - (white / bright))); - this->pinb_duty_ = bright; - } else if (white < 0.45) { - this->pina_duty_ = bright; - this->pinb_duty_ = white; - } else { - this->pina_duty_ = bright; - this->pinb_duty_ = bright; - } + state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); } protected: @@ -68,7 +51,6 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { output::FloatOutput *pinb_pin_; float pina_duty_ = 0; float pinb_duty_ = 0; - float duty_off_ = 0; bool forward_direction_ = false; }; diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 507ac77a28..60e8943e67 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -1,5 +1,6 @@ #include "hdc1080.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace hdc1080 { @@ -36,18 +37,30 @@ void HDC1080Component::dump_config() { } void HDC1080Component::update() { uint16_t raw_temp; - if (!this->read_byte_16(HDC1080_CMD_TEMPERATURE, &raw_temp, 20)) { + if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } + delay(20); + if (this->read(reinterpret_cast(&raw_temp), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_temp = i2c::i2ctohs(raw_temp); float temp = raw_temp * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40 this->temperature_->publish_state(temp); uint16_t raw_humidity; - if (!this->read_byte_16(HDC1080_CMD_HUMIDITY, &raw_humidity, 20)) { + if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } + delay(20); + if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_humidity = i2c::i2ctohs(raw_humidity); float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100 this->humidity_->publish_state(humidity); diff --git a/esphome/components/hdc1080/sensor.py b/esphome/components/hdc1080/sensor.py index 26ec3ad0a9..39727f7159 100644 --- a/esphome/components/hdc1080/sensor.py +++ b/esphome/components/hdc1080/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HDC1080Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/heatpumpir/__init__.py b/esphome/components/heatpumpir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py new file mode 100644 index 0000000000..36e56aa5da --- /dev/null +++ b/esphome/components/heatpumpir/climate.py @@ -0,0 +1,114 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import ( + CONF_ID, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_PROTOCOL, + CONF_VISUAL, +) + +CODEOWNERS = ["@rob-deutsch"] + +AUTO_LOAD = ["climate_ir"] + +heatpumpir_ns = cg.esphome_ns.namespace("heatpumpir") +HeatpumpIRClimate = heatpumpir_ns.class_("HeatpumpIRClimate", climate_ir.ClimateIR) + +Protocol = heatpumpir_ns.enum("Protocol") +PROTOCOLS = { + "aux": Protocol.PROTOCOL_AUX, + "ballu": Protocol.PROTOCOL_BALLU, + "carrier_mca": Protocol.PROTOCOL_CARRIER_MCA, + "carrier_nqv": Protocol.PROTOCOL_CARRIER_NQV, + "daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417, + "daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480, + "daikin": Protocol.PROTOCOL_DAIKIN, + "fuego": Protocol.PROTOCOL_FUEGO, + "fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ, + "gree": Protocol.PROTOCOL_GREE, + "greeya": Protocol.PROTOCOL_GREEYAA, + "greeyan": Protocol.PROTOCOL_GREEYAN, + "hisense_aud": Protocol.PROTOCOL_HISENSE_AUD, + "hitachi": Protocol.PROTOCOL_HITACHI, + "hyundai": Protocol.PROTOCOL_HYUNDAI, + "ivt": Protocol.PROTOCOL_IVT, + "midea": Protocol.PROTOCOL_MIDEA, + "mitsubishi_fa": Protocol.PROTOCOL_MITSUBISHI_FA, + "mitsubishi_fd": Protocol.PROTOCOL_MITSUBISHI_FD, + "mitsubishi_fe": Protocol.PROTOCOL_MITSUBISHI_FE, + "mitsubishi_heavy_fdtc": Protocol.PROTOCOL_MITSUBISHI_HEAVY_FDTC, + "mitsubishi_heavy_zj": Protocol.PROTOCOL_MITSUBISHI_HEAVY_ZJ, + "mitsubishi_heavy_zm": Protocol.PROTOCOL_MITSUBISHI_HEAVY_ZM, + "mitsubishi_heavy_zmp": Protocol.PROTOCOL_MITSUBISHI_HEAVY_ZMP, + "mitsubishi_heavy_kj": Protocol.PROTOCOL_MITSUBISHI_KJ, + "mitsubishi_msc": Protocol.PROTOCOL_MITSUBISHI_MSC, + "mitsubishi_msy": Protocol.PROTOCOL_MITSUBISHI_MSY, + "mitsubishi_sez": Protocol.PROTOCOL_MITSUBISHI_SEZ, + "panasonic_ckp": Protocol.PROTOCOL_PANASONIC_CKP, + "panasonic_dke": Protocol.PROTOCOL_PANASONIC_DKE, + "panasonic_jke": Protocol.PROTOCOL_PANASONIC_JKE, + "panasonic_lke": Protocol.PROTOCOL_PANASONIC_LKE, + "panasonic_nke": Protocol.PROTOCOL_PANASONIC_NKE, + "samsung_aqv": Protocol.PROTOCOL_SAMSUNG_AQV, + "samsung_fjm": Protocol.PROTOCOL_SAMSUNG_FJM, + "sharp": Protocol.PROTOCOL_SHARP, + "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, + "toshiba": Protocol.PROTOCOL_TOSHIBA, +} + +CONF_HORIZONTAL_DEFAULT = "horizontal_default" +HorizontalDirections = heatpumpir_ns.enum("HorizontalDirections") +HORIZONTAL_DIRECTIONS = { + "auto": HorizontalDirections.HORIZONTAL_DIRECTION_AUTO, + "middle": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE, + "left": HorizontalDirections.HORIZONTAL_DIRECTION_LEFT, + "mleft": HorizontalDirections.HORIZONTAL_DIRECTION_MLEFT, + "mright": HorizontalDirections.HORIZONTAL_DIRECTION_MRIGHT, + "right": HorizontalDirections.HORIZONTAL_DIRECTION_RIGHT, +} + +CONF_VERTICAL_DEFAULT = "vertical_default" +VerticalDirections = heatpumpir_ns.enum("VerticalDirections") +VERTICAL_DIRECTIONS = { + "auto": VerticalDirections.VERTICAL_DIRECTION_AUTO, + "up": VerticalDirections.VERTICAL_DIRECTION_UP, + "mup": VerticalDirections.VERTICAL_DIRECTION_MUP, + "middle": VerticalDirections.VERTICAL_DIRECTION_MIDDLE, + "mdown": VerticalDirections.VERTICAL_DIRECTION_MDOWN, + "down": VerticalDirections.VERTICAL_DIRECTION_DOWN, +} + +CONFIG_SCHEMA = cv.All( + climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HeatpumpIRClimate), + cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), + cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), + cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS), + cv.Required(CONF_MIN_TEMPERATURE): cv.temperature, + cv.Required(CONF_MAX_TEMPERATURE): cv.temperature, + } + ), + cv.only_with_arduino, +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if CONF_VISUAL not in config: + config[CONF_VISUAL] = {} + visual = config[CONF_VISUAL] + if CONF_MAX_TEMPERATURE not in visual: + visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE] + if CONF_MIN_TEMPERATURE not in visual: + visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE] + yield climate_ir.register_climate_ir(var, config) + cg.add(var.set_protocol(config[CONF_PROTOCOL])) + cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) + cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) + cg.add(var.set_max_temperature(config[CONF_MIN_TEMPERATURE])) + cg.add(var.set_min_temperature(config[CONF_MAX_TEMPERATURE])) + + cg.add_library("tonia/HeatpumpIR", "1.0.15") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp new file mode 100644 index 0000000000..8d9fc962c0 --- /dev/null +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -0,0 +1,183 @@ +#include "heatpumpir.h" + +#ifdef USE_ARDUINO + +#include +#include "ir_sender_esphome.h" +#include "HeatpumpIRFactory.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace heatpumpir { + +static const char *const TAG = "heatpumpir.climate"; + +const std::map> PROTOCOL_CONSTRUCTOR_MAP = { + {PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT + {PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT + {PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT + {PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT + {PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT + {PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT + {PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT + {PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT + {PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT + {PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT + {PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT + {PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT + {PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT + {PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT + {PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT + {PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT + {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT + {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT +}; + +void HeatpumpIRClimate::setup() { + auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_); + if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) { + ESP_LOGE(TAG, "Invalid protocol"); + return; + } + this->heatpump_ir_ = protocol_constructor->second(); + climate_ir::ClimateIR::setup(); +} + +void HeatpumpIRClimate::transmit_state() { + uint8_t power_mode_cmd; + uint8_t operating_mode_cmd; + uint8_t temperature_cmd; + uint8_t fan_speed_cmd; + + uint8_t swing_v_cmd; + switch (default_vertical_direction_) { + case VERTICAL_DIRECTION_AUTO: + swing_v_cmd = VDIR_AUTO; + break; + case VERTICAL_DIRECTION_UP: + swing_v_cmd = VDIR_UP; + break; + case VERTICAL_DIRECTION_MUP: + swing_v_cmd = VDIR_MUP; + break; + case VERTICAL_DIRECTION_MIDDLE: + swing_v_cmd = VDIR_MIDDLE; + break; + case VERTICAL_DIRECTION_MDOWN: + swing_v_cmd = VDIR_MDOWN; + break; + case VERTICAL_DIRECTION_DOWN: + swing_v_cmd = VDIR_DOWN; + break; + default: + ESP_LOGE(TAG, "Invalid default vertical direction"); + return; + } + if ((this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)) { + swing_v_cmd = VDIR_SWING; + } + + uint8_t swing_h_cmd; + switch (default_horizontal_direction_) { + case HORIZONTAL_DIRECTION_AUTO: + swing_h_cmd = HDIR_AUTO; + break; + case HORIZONTAL_DIRECTION_MIDDLE: + swing_h_cmd = HDIR_MIDDLE; + break; + case HORIZONTAL_DIRECTION_LEFT: + swing_h_cmd = HDIR_LEFT; + break; + case HORIZONTAL_DIRECTION_MLEFT: + swing_h_cmd = HDIR_MLEFT; + break; + case HORIZONTAL_DIRECTION_MRIGHT: + swing_h_cmd = HDIR_MRIGHT; + break; + case HORIZONTAL_DIRECTION_RIGHT: + swing_h_cmd = HDIR_RIGHT; + break; + default: + ESP_LOGE(TAG, "Invalid default horizontal direction"); + return; + } + if ((this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)) { + swing_h_cmd = HDIR_SWING; + } + + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) { + case climate::CLIMATE_FAN_LOW: + fan_speed_cmd = FAN_2; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed_cmd = FAN_3; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed_cmd = FAN_4; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed_cmd = FAN_AUTO; + break; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + power_mode_cmd = POWER_ON; + operating_mode_cmd = MODE_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + power_mode_cmd = POWER_ON; + operating_mode_cmd = MODE_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + power_mode_cmd = POWER_ON; + operating_mode_cmd = MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + power_mode_cmd = POWER_ON; + operating_mode_cmd = MODE_FAN; + break; + case climate::CLIMATE_MODE_DRY: + power_mode_cmd = POWER_ON; + operating_mode_cmd = MODE_DRY; + break; + case climate::CLIMATE_MODE_OFF: + default: + power_mode_cmd = POWER_OFF; + operating_mode_cmd = MODE_AUTO; + break; + } + + temperature_cmd = (uint8_t) clamp(this->target_temperature, this->min_temperature_, this->max_temperature_); + + IRSenderESPHome esp_sender(0, this->transmitter_); + + heatpump_ir_->send(esp_sender, power_mode_cmd, operating_mode_cmd, fan_speed_cmd, temperature_cmd, swing_v_cmd, + swing_h_cmd); +} + +} // namespace heatpumpir +} // namespace esphome + +#endif diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h new file mode 100644 index 0000000000..e2d2b45dc4 --- /dev/null +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -0,0 +1,116 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/components/climate_ir/climate_ir.h" + +// Forward-declare HeatpumpIR class from library. We cannot include its header here because it has unnamespaced defines +// that conflict with ESPHome. +class HeatpumpIR; + +namespace esphome { +namespace heatpumpir { + +// Simple enum to represent protocols. +enum Protocol { + PROTOCOL_AUX, + PROTOCOL_BALLU, + PROTOCOL_CARRIER_MCA, + PROTOCOL_CARRIER_NQV, + PROTOCOL_DAIKIN_ARC417, + PROTOCOL_DAIKIN_ARC480, + PROTOCOL_DAIKIN, + PROTOCOL_FUEGO, + PROTOCOL_FUJITSU_AWYZ, + PROTOCOL_GREE, + PROTOCOL_GREEYAA, + PROTOCOL_GREEYAN, + PROTOCOL_HISENSE_AUD, + PROTOCOL_HITACHI, + PROTOCOL_HYUNDAI, + PROTOCOL_IVT, + PROTOCOL_MIDEA, + PROTOCOL_MITSUBISHI_FA, + PROTOCOL_MITSUBISHI_FD, + PROTOCOL_MITSUBISHI_FE, + PROTOCOL_MITSUBISHI_HEAVY_FDTC, + PROTOCOL_MITSUBISHI_HEAVY_ZJ, + PROTOCOL_MITSUBISHI_HEAVY_ZM, + PROTOCOL_MITSUBISHI_HEAVY_ZMP, + PROTOCOL_MITSUBISHI_KJ, + PROTOCOL_MITSUBISHI_MSC, + PROTOCOL_MITSUBISHI_MSY, + PROTOCOL_MITSUBISHI_SEZ, + PROTOCOL_PANASONIC_CKP, + PROTOCOL_PANASONIC_DKE, + PROTOCOL_PANASONIC_JKE, + PROTOCOL_PANASONIC_LKE, + PROTOCOL_PANASONIC_NKE, + PROTOCOL_SAMSUNG_AQV, + PROTOCOL_SAMSUNG_FJM, + PROTOCOL_SHARP, + PROTOCOL_TOSHIBA_DAISEIKAI, + PROTOCOL_TOSHIBA, +}; + +// Simple enum to represent horizontal directios +enum HorizontalDirection { + HORIZONTAL_DIRECTION_AUTO = 0, + HORIZONTAL_DIRECTION_MIDDLE = 1, + HORIZONTAL_DIRECTION_LEFT = 2, + HORIZONTAL_DIRECTION_MLEFT = 3, + HORIZONTAL_DIRECTION_MRIGHT = 4, + HORIZONTAL_DIRECTION_RIGHT = 5, +}; + +// Simple enum to represent vertical directions +enum VerticalDirection { + VERTICAL_DIRECTION_AUTO = 0, + VERTICAL_DIRECTION_UP = 1, + VERTICAL_DIRECTION_MUP = 2, + VERTICAL_DIRECTION_MIDDLE = 3, + VERTICAL_DIRECTION_MDOWN = 4, + VERTICAL_DIRECTION_DOWN = 5, +}; + +// Temperature +const float TEMP_MIN = 0; // Celsius +const float TEMP_MAX = 100; // Celsius + +class HeatpumpIRClimate : public climate_ir::ClimateIR { + public: + HeatpumpIRClimate() + : climate_ir::ClimateIR( + TEMP_MIN, TEMP_MAX, 1.0f, true, true, + std::set{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, + std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} + void setup() override; + void set_protocol(Protocol protocol) { this->protocol_ = protocol; } + 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; + } + + void set_max_temperature(float temperature) { this->max_temperature_ = temperature; } + void set_min_temperature(float temperature) { this->min_temperature_ = temperature; } + + protected: + HeatpumpIR *heatpump_ir_; + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + Protocol protocol_; + HorizontalDirection default_horizontal_direction_; + VerticalDirection default_vertical_direction_; + + float max_temperature_; + float min_temperature_; +}; + +} // namespace heatpumpir +} // namespace esphome + +#endif diff --git a/esphome/components/heatpumpir/ir_sender_esphome.cpp b/esphome/components/heatpumpir/ir_sender_esphome.cpp new file mode 100644 index 0000000000..24c7933563 --- /dev/null +++ b/esphome/components/heatpumpir/ir_sender_esphome.cpp @@ -0,0 +1,32 @@ +#include "ir_sender_esphome.h" + +#ifdef USE_ARDUINO + +namespace esphome { +namespace heatpumpir { + +void IRSenderESPHome::setFrequency(int frequency) { // NOLINT(readability-identifier-naming) + auto data = transmit_.get_data(); + data->set_carrier_frequency(1000 * frequency); +} + +// Send an IR 'mark' symbol, i.e. transmitter ON +void IRSenderESPHome::mark(int mark_length) { + auto data = transmit_.get_data(); + data->mark(mark_length); +} + +// Send an IR 'space' symbol, i.e. transmitter OFF +void IRSenderESPHome::space(int space_length) { + if (space_length) { + auto data = transmit_.get_data(); + data->space(space_length); + } else { + transmit_.perform(); + } +} + +} // namespace heatpumpir +} // namespace esphome + +#endif diff --git a/esphome/components/heatpumpir/ir_sender_esphome.h b/esphome/components/heatpumpir/ir_sender_esphome.h new file mode 100644 index 0000000000..24e8ba9883 --- /dev/null +++ b/esphome/components/heatpumpir/ir_sender_esphome.h @@ -0,0 +1,27 @@ +#pragma once + +#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 { +namespace heatpumpir { + +class IRSenderESPHome : public IRSender { + public: + IRSenderESPHome(uint8_t pin, remote_transmitter::RemoteTransmitterComponent *transmitter) + : IRSender(pin), 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_; +}; + +} // namespace heatpumpir +} // namespace esphome + +#endif diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 35b3d17358..067ea39d07 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -155,7 +155,7 @@ void HitachiClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: set_mode_(HITACHI_AC344_MODE_HEAT); break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: set_mode_(HITACHI_AC344_MODE_AUTO); break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -165,7 +165,7 @@ void HitachiClimate::transmit_state() { set_power_(false); break; default: - ESP_LOGW(TAG, "Unsupported mode: %s", climate_mode_to_string(this->mode)); + ESP_LOGW(TAG, "Unsupported mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); } set_temp_(static_cast(this->target_temperature)); @@ -251,7 +251,7 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { this->mode = climate::CLIMATE_MODE_HEAT; break; case HITACHI_AC344_MODE_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; case HITACHI_AC344_MODE_FAN: this->mode = climate::CLIMATE_MODE_FAN_ONLY; diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index 3126ef0493..c34f033d92 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -79,11 +79,10 @@ const uint16_t HITACHI_AC344_BITS = HITACHI_AC344_STATE_LENGTH * 8; class HitachiClimate : public climate_ir::ClimateIR { public: HitachiClimate() - : climate_ir::ClimateIR( - HITACHI_AC344_TEMP_MIN, HITACHI_AC344_TEMP_MAX, 1.0F, true, true, - std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, - std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}) {} + : climate_ir::ClimateIR(HITACHI_AC344_TEMP_MIN, HITACHI_AC344_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_HORIZONTAL}) {} protected: uint8_t remote_state_[HITACHI_AC344_STATE_LENGTH]{0x01, 0x10, 0x00, 0x40, 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00, diff --git a/esphome/components/hitachi_ac424/__init__.py b/esphome/components/hitachi_ac424/__init__.py new file mode 100644 index 0000000000..10f2c27fe8 --- /dev/null +++ b/esphome/components/hitachi_ac424/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@sourabhjaiswal"] diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py new file mode 100644 index 0000000000..33532230df --- /dev/null +++ b/esphome/components/hitachi_ac424/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"] + +hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") +HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HitachiClimate), + } +) + + +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/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp new file mode 100644 index 0000000000..2e5423a37a --- /dev/null +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -0,0 +1,368 @@ +#include "hitachi_ac424.h" + +namespace esphome { +namespace hitachi_ac424 { + +static const char *const TAG = "climate.hitachi_ac424"; + +void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, const uint8_t data) { + if (offset >= 8 || !nbits) + return; // Short circuit as it won't change. + // Calculate the mask for the supplied value. + uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); + // Calculate the mask & clear the space for the data. + // Clear the destination bits. + *dst &= ~(uint8_t)(mask << offset); + // Merge in the data. + *dst |= ((data & mask) << offset); +} + +void set_bit(uint8_t *const data, const uint8_t position, const bool on) { + uint8_t mask = 1 << position; + if (on) + *data |= mask; + else + *data &= ~mask; +} + +uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) { + for (uint16_t i = 1; i < length; i += 2) { + // Code done this way to avoid a compiler warning bug. + uint8_t inv = ~*(ptr + i - 1); + *(ptr + i) = inv; + } + return ptr; +} + +bool HitachiClimate::get_power_() { return remote_state_[HITACHI_AC424_POWER_BYTE] == HITACHI_AC424_POWER_ON; } + +void HitachiClimate::set_power_(bool on) { + set_button_(HITACHI_AC424_BUTTON_POWER); + remote_state_[HITACHI_AC424_POWER_BYTE] = on ? HITACHI_AC424_POWER_ON : HITACHI_AC424_POWER_OFF; +} + +uint8_t HitachiClimate::get_mode_() { return remote_state_[HITACHI_AC424_MODE_BYTE] & 0xF; } + +void HitachiClimate::set_mode_(uint8_t mode) { + uint8_t new_mode = mode; + switch (mode) { + // Fan mode sets a special temp. + case HITACHI_AC424_MODE_FAN: + set_temp_(HITACHI_AC424_TEMP_FAN, false); + break; + case HITACHI_AC424_MODE_HEAT: + case HITACHI_AC424_MODE_COOL: + case HITACHI_AC424_MODE_DRY: + break; + default: + new_mode = HITACHI_AC424_MODE_COOL; + } + set_bits(&remote_state_[HITACHI_AC424_MODE_BYTE], 0, 4, new_mode); + if (new_mode != HITACHI_AC424_MODE_FAN) + set_temp_(previous_temp_); + set_fan_(get_fan_()); // Reset the fan speed after the mode change. + set_power_(true); +} + +void HitachiClimate::set_temp_(uint8_t celsius, bool set_previous) { + uint8_t temp; + temp = std::min(celsius, HITACHI_AC424_TEMP_MAX); + temp = std::max(temp, HITACHI_AC424_TEMP_MIN); + set_bits(&remote_state_[HITACHI_AC424_TEMP_BYTE], HITACHI_AC424_TEMP_OFFSET, HITACHI_AC424_TEMP_SIZE, temp); + if (previous_temp_ > temp) + set_button_(HITACHI_AC424_BUTTON_TEMP_DOWN); + else if (previous_temp_ < temp) + set_button_(HITACHI_AC424_BUTTON_TEMP_UP); + if (set_previous) + previous_temp_ = temp; +} + +uint8_t HitachiClimate::get_fan_() { return remote_state_[HITACHI_AC424_FAN_BYTE] >> 4 & 0xF; } + +void HitachiClimate::set_fan_(uint8_t speed) { + uint8_t new_speed = std::max(speed, HITACHI_AC424_FAN_MIN); + uint8_t fan_max = HITACHI_AC424_FAN_MAX; + + // Only 2 x low speeds in Dry mode or Auto + if (get_mode_() == HITACHI_AC424_MODE_DRY && speed == HITACHI_AC424_FAN_AUTO) { + fan_max = HITACHI_AC424_FAN_AUTO; + } else if (get_mode_() == HITACHI_AC424_MODE_DRY) { + fan_max = HITACHI_AC424_FAN_MAX_DRY; + } else if (get_mode_() == HITACHI_AC424_MODE_FAN && speed == HITACHI_AC424_FAN_AUTO) { + // Fan Mode does not have auto. Set to safe low + new_speed = HITACHI_AC424_FAN_MIN; + } + + new_speed = std::min(new_speed, fan_max); + // Handle the setting the button value if we are going to change the value. + if (new_speed != get_fan_()) + set_button_(HITACHI_AC424_BUTTON_FAN); + // Set the values + + set_bits(&remote_state_[HITACHI_AC424_FAN_BYTE], 4, 4, new_speed); + remote_state_[9] = 0x92; + + // When fan is at min/max, additional bytes seem to be set + if (new_speed == HITACHI_AC424_FAN_MIN) + remote_state_[9] = 0x98; + remote_state_[29] = 0x01; +} + +void HitachiClimate::set_swing_v_toggle_(bool on) { + uint8_t button = get_button_(); // Get the current button value. + if (on) + button = HITACHI_AC424_BUTTON_SWINGV; // Set the button to SwingV. + else if (button == HITACHI_AC424_BUTTON_SWINGV) // Asked to unset it + // It was set previous, so use Power as a default + button = HITACHI_AC424_BUTTON_POWER; + set_button_(button); +} + +bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC424_BUTTON_SWINGV; } + +void HitachiClimate::set_swing_v_(bool on) { + set_swing_v_toggle_(on); // Set the button value. + set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on); +} + +bool HitachiClimate::get_swing_v_() { + return HITACHI_AC424_GETBIT8(remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET); +} + +void HitachiClimate::set_swing_h_(uint8_t position) { + if (position > HITACHI_AC424_SWINGH_LEFT_MAX) + return set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + set_bits(&remote_state_[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, HITACHI_AC424_SWINGH_SIZE, position); + set_button_(HITACHI_AC424_BUTTON_SWINGH); +} + +uint8_t HitachiClimate::get_swing_h_() { + return HITACHI_AC424_GETBITS8(remote_state_[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, + HITACHI_AC424_SWINGH_SIZE); +} + +uint8_t HitachiClimate::get_button_() { return remote_state_[HITACHI_AC424_BUTTON_BYTE]; } + +void HitachiClimate::set_button_(uint8_t button) { remote_state_[HITACHI_AC424_BUTTON_BYTE] = button; } + +void HitachiClimate::transmit_state() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + set_mode_(HITACHI_AC424_MODE_COOL); + break; + case climate::CLIMATE_MODE_DRY: + set_mode_(HITACHI_AC424_MODE_DRY); + break; + case climate::CLIMATE_MODE_HEAT: + set_mode_(HITACHI_AC424_MODE_HEAT); + break; + case climate::CLIMATE_MODE_HEAT_COOL: + set_mode_(HITACHI_AC424_MODE_AUTO); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + set_mode_(HITACHI_AC424_MODE_FAN); + break; + case climate::CLIMATE_MODE_OFF: + set_power_(false); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); + } + + set_temp_(static_cast(this->target_temperature)); + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + set_fan_(HITACHI_AC424_FAN_LOW); + break; + case climate::CLIMATE_FAN_MEDIUM: + set_fan_(HITACHI_AC424_FAN_MEDIUM); + break; + case climate::CLIMATE_FAN_HIGH: + set_fan_(HITACHI_AC424_FAN_HIGH); + break; + case climate::CLIMATE_FAN_ON: + case climate::CLIMATE_FAN_AUTO: + default: + set_fan_(HITACHI_AC424_FAN_AUTO); + } + + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + set_swing_v_(true); + set_swing_h_(HITACHI_AC424_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_VERTICAL: + set_swing_v_(true); + set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + set_swing_v_(false); + set_swing_h_(HITACHI_AC424_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_OFF: + set_swing_v_(false); + set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + break; + } + + // TODO: find change value to set button, now always set to power button + set_button_(HITACHI_AC424_BUTTON_POWER); + + invert_byte_pairs(remote_state_ + 3, HITACHI_AC424_STATE_LENGTH - 3); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(HITACHI_AC424_FREQ); + + uint8_t repeat = 0; + for (uint8_t r = 0; r <= repeat; r++) { + // Header + data->item(HITACHI_AC424_HDR_MARK, HITACHI_AC424_HDR_SPACE); + // Data + for (uint8_t i : remote_state_) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(HITACHI_AC424_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? HITACHI_AC424_ONE_SPACE : HITACHI_AC424_ZERO_SPACE); + } + } + // Footer + data->item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_MIN_GAP); + } + transmit.perform(); + + dump_state_("Sent", remote_state_); +} + +bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { + uint8_t power = remote_state[HITACHI_AC424_POWER_BYTE]; + ESP_LOGV(TAG, "Power: %02X %02X", remote_state[HITACHI_AC424_POWER_BYTE], power); + uint8_t mode = remote_state[HITACHI_AC424_MODE_BYTE] & 0xF; + ESP_LOGV(TAG, "Mode: %02X %02X", remote_state[HITACHI_AC424_MODE_BYTE], mode); + if (power == HITACHI_AC424_POWER_ON) { + switch (mode) { + case HITACHI_AC424_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case HITACHI_AC424_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case HITACHI_AC424_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case HITACHI_AC424_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case HITACHI_AC424_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + return true; +} + +bool HitachiClimate::parse_temperature_(const uint8_t remote_state[]) { + uint8_t temperature = + HITACHI_AC424_GETBITS8(remote_state[HITACHI_AC424_TEMP_BYTE], HITACHI_AC424_TEMP_OFFSET, HITACHI_AC424_TEMP_SIZE); + this->target_temperature = temperature; + ESP_LOGV(TAG, "Temperature: %02X %02u %04f", remote_state[HITACHI_AC424_TEMP_BYTE], temperature, + this->target_temperature); + return true; +} + +bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) { + uint8_t fan_mode = remote_state[HITACHI_AC424_FAN_BYTE] >> 4 & 0xF; + ESP_LOGV(TAG, "Fan: %02X %02X", remote_state[HITACHI_AC424_FAN_BYTE], fan_mode); + switch (fan_mode) { + case HITACHI_AC424_FAN_MIN: + case HITACHI_AC424_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case HITACHI_AC424_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case HITACHI_AC424_FAN_HIGH: + case HITACHI_AC424_FAN_MAX: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case HITACHI_AC424_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + return true; +} + +bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) { + uint8_t swing_modeh = HITACHI_AC424_GETBITS8(remote_state[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, + HITACHI_AC424_SWINGH_SIZE); + ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC424_SWINGH_BYTE], swing_modeh); + + if ((swing_modeh & 0x7) == 0x0) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else if ((swing_modeh & 0x3) == 0x3) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } else { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } + + return true; +} + +bool HitachiClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(HITACHI_AC424_HDR_MARK, HITACHI_AC424_HDR_SPACE)) { + ESP_LOGVV(TAG, "Header fail"); + return false; + } + + uint8_t recv_state[HITACHI_AC424_STATE_LENGTH] = {0}; + // Read all bytes. + for (uint8_t pos = 0; pos < HITACHI_AC424_STATE_LENGTH; pos++) { + // Read bit + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_ONE_SPACE)) + recv_state[pos] |= 1 << bit; + else if (!data.expect_item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", pos, bit); + return false; + } + } + } + + // Validate footer + if (!data.expect_mark(HITACHI_AC424_BIT_MARK)) { + ESP_LOGVV(TAG, "Footer fail"); + return false; + } + + dump_state_("Recv", recv_state); + + // parse mode + this->parse_mode_(recv_state); + // parse temperature + this->parse_temperature_(recv_state); + // parse fan + this->parse_fan_(recv_state); + // parse swingv + this->parse_swing_(recv_state); + this->publish_state(); + for (uint8_t i = 0; i < HITACHI_AC424_STATE_LENGTH; i++) + remote_state_[i] = recv_state[i]; + + return true; +} + +void HitachiClimate::dump_state_(const char action[], uint8_t state[]) { + for (uint16_t i = 0; i < HITACHI_AC424_STATE_LENGTH - 10; i += 10) { + ESP_LOGV(TAG, "%s: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", action, state[i + 0], state[i + 1], + state[i + 2], state[i + 3], state[i + 4], state[i + 5], state[i + 6], state[i + 7], state[i + 8], + state[i + 9]); + } + ESP_LOGV(TAG, "%s: %02X %02X %02X", action, state[40], state[41], state[42]); +} + +} // namespace hitachi_ac424 +} // namespace esphome diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.h b/esphome/components/hitachi_ac424/hitachi_ac424.h new file mode 100644 index 0000000000..1005aa6df7 --- /dev/null +++ b/esphome/components/hitachi_ac424/hitachi_ac424.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace hitachi_ac424 { + +const uint16_t HITACHI_AC424_HDR_MARK = 3416; // ac +const uint16_t HITACHI_AC424_HDR_SPACE = 1604; // ac +const uint16_t HITACHI_AC424_BIT_MARK = 463; +const uint16_t HITACHI_AC424_ONE_SPACE = 1208; +const uint16_t HITACHI_AC424_ZERO_SPACE = 372; +const uint32_t HITACHI_AC424_MIN_GAP = 100000; // just a guess. +const uint16_t HITACHI_AC424_FREQ = 38000; // Hz. + +const uint8_t HITACHI_AC424_BUTTON_BYTE = 11; +const uint8_t HITACHI_AC424_BUTTON_POWER = 0x13; +const uint8_t HITACHI_AC424_BUTTON_SLEEP = 0x31; +const uint8_t HITACHI_AC424_BUTTON_MODE = 0x41; +const uint8_t HITACHI_AC424_BUTTON_FAN = 0x42; +const uint8_t HITACHI_AC424_BUTTON_TEMP_DOWN = 0x43; +const uint8_t HITACHI_AC424_BUTTON_TEMP_UP = 0x44; +const uint8_t HITACHI_AC424_BUTTON_SWINGV = 0x81; +const uint8_t HITACHI_AC424_BUTTON_SWINGH = 0x8C; +const uint8_t HITACHI_AC424_BUTTON_MILDEWPROOF = 0xE2; + +const uint8_t HITACHI_AC424_TEMP_BYTE = 13; +const uint8_t HITACHI_AC424_TEMP_OFFSET = 2; +const uint8_t HITACHI_AC424_TEMP_SIZE = 6; +const uint8_t HITACHI_AC424_TEMP_MIN = 16; // 16C +const uint8_t HITACHI_AC424_TEMP_MAX = 32; // 32C +const uint8_t HITACHI_AC424_TEMP_FAN = 27; // 27C + +const uint8_t HITACHI_AC424_TIMER_BYTE = 15; + +const uint8_t HITACHI_AC424_MODE_BYTE = 25; +const uint8_t HITACHI_AC424_MODE_FAN = 1; +const uint8_t HITACHI_AC424_MODE_COOL = 3; +const uint8_t HITACHI_AC424_MODE_DRY = 5; +const uint8_t HITACHI_AC424_MODE_HEAT = 6; +const uint8_t HITACHI_AC424_MODE_AUTO = 14; +const uint8_t HITACHI_AC424_MODE_POWERFUL = 19; + +const uint8_t HITACHI_AC424_FAN_BYTE = HITACHI_AC424_MODE_BYTE; +const uint8_t HITACHI_AC424_FAN_MIN = 1; +const uint8_t HITACHI_AC424_FAN_LOW = 2; +const uint8_t HITACHI_AC424_FAN_MEDIUM = 3; +const uint8_t HITACHI_AC424_FAN_HIGH = 4; +const uint8_t HITACHI_AC424_FAN_AUTO = 5; +const uint8_t HITACHI_AC424_FAN_MAX = 6; +const uint8_t HITACHI_AC424_FAN_MAX_DRY = 2; + +const uint8_t HITACHI_AC424_POWER_BYTE = 27; +const uint8_t HITACHI_AC424_POWER_ON = 0xF1; +const uint8_t HITACHI_AC424_POWER_OFF = 0xE1; + +const uint8_t HITACHI_AC424_SWINGH_BYTE = 35; +const uint8_t HITACHI_AC424_SWINGH_OFFSET = 0; // Mask 0b00000xxx +const uint8_t HITACHI_AC424_SWINGH_SIZE = 3; // Mask 0b00000xxx +const uint8_t HITACHI_AC424_SWINGH_AUTO = 0; // 0b000 +const uint8_t HITACHI_AC424_SWINGH_RIGHT_MAX = 1; // 0b001 +const uint8_t HITACHI_AC424_SWINGH_RIGHT = 2; // 0b010 +const uint8_t HITACHI_AC424_SWINGH_MIDDLE = 3; // 0b011 +const uint8_t HITACHI_AC424_SWINGH_LEFT = 4; // 0b100 +const uint8_t HITACHI_AC424_SWINGH_LEFT_MAX = 5; // 0b101 + +const uint8_t HITACHI_AC424_SWINGV_BYTE = 37; +const uint8_t HITACHI_AC424_SWINGV_OFFSET = 5; // Mask 0b00x00000 + +const uint8_t HITACHI_AC424_MILDEWPROOF_BYTE = HITACHI_AC424_SWINGV_BYTE; +const uint8_t HITACHI_AC424_MILDEWPROOF_OFFSET = 2; // Mask 0b00000x00 + +const uint16_t HITACHI_AC424_STATE_LENGTH = 53; +const uint16_t HITACHI_AC424_BITS = HITACHI_AC424_STATE_LENGTH * 8; + +#define HITACHI_AC424_GETBIT8(a, b) ((a) & ((uint8_t) 1 << (b))) +#define HITACHI_AC424_GETBITS8(data, offset, size) \ + (((data) & (((uint8_t) UINT8_MAX >> (8 - (size))) << (offset))) >> (offset)) + +class HitachiClimate : public climate_ir::ClimateIR { + public: + HitachiClimate() + : climate_ir::ClimateIR(HITACHI_AC424_TEMP_MIN, HITACHI_AC424_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_HORIZONTAL}) {} + + protected: + uint8_t remote_state_[HITACHI_AC424_STATE_LENGTH]{ + 0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 0x33, 0x92, 0x6D, 0x13, 0xEC, 0x5C, 0xA3, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x53, 0xAC, 0xF1, 0x0E, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0x03, + 0xFC, 0x01, 0xFE, 0x88, 0x77, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}; + uint8_t previous_temp_{27}; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + bool get_power_(); + void set_power_(bool on); + uint8_t get_mode_(); + void set_mode_(uint8_t mode); + void set_temp_(uint8_t celsius, bool set_previous = false); + uint8_t get_fan_(); + void set_fan_(uint8_t speed); + void set_swing_v_toggle_(bool on); + bool get_swing_v_toggle_(); + void set_swing_v_(bool on); + bool get_swing_v_(); + void set_swing_h_(uint8_t position); + uint8_t get_swing_h_(); + uint8_t get_button_(); + void set_button_(uint8_t button); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_mode_(const uint8_t remote_state[]); + bool parse_temperature_(const uint8_t remote_state[]); + bool parse_fan_(const uint8_t remote_state[]); + bool parse_swing_(const uint8_t remote_state[]); + bool parse_state_frame_(const uint8_t frame[]); + void dump_state_(const char action[], uint8_t remote_state[]); +}; + +} // namespace hitachi_ac424 +} // namespace esphome diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 356dbd0bf4..ecdaa07ab2 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -6,15 +6,32 @@ namespace hlw8012 { static const char *const TAG = "hlw8012"; +// valid for HLW8012 and CSE7759 static const uint32_t HLW8012_CLOCK_FREQUENCY = 3579000; -static const float HLW8012_REFERENCE_VOLTAGE = 2.43f; void HLW8012Component::setup() { + float reference_voltage = 0; ESP_LOGCONFIG(TAG, "Setting up HLW8012..."); this->sel_pin_->setup(); this->sel_pin_->digital_write(this->current_mode_); this->cf_store_.pulse_counter_setup(this->cf_pin_); this->cf1_store_.pulse_counter_setup(this->cf1_pin_); + + // Initialize multipliers + if (this->sensor_model_ == HLW8012_SENSOR_MODEL_BL0937) { + reference_voltage = 1.218f; + this->power_multiplier_ = + reference_voltage * reference_voltage * this->voltage_divider_ / this->current_resistor_ / 1721506.0f; + this->current_multiplier_ = reference_voltage / this->current_resistor_ / 94638.0f; + this->voltage_multiplier_ = reference_voltage * this->voltage_divider_ / 15397.0f; + } else { + // HLW8012 and CSE7759 have same reference specs + reference_voltage = 2.43f; + this->power_multiplier_ = reference_voltage * reference_voltage * this->voltage_divider_ / this->current_resistor_ * + 64.0f / 24.0f / HLW8012_CLOCK_FREQUENCY; + this->current_multiplier_ = reference_voltage / this->current_resistor_ * 512.0f / 24.0f / HLW8012_CLOCK_FREQUENCY; + this->voltage_multiplier_ = reference_voltage * this->voltage_divider_ * 256.0f / HLW8012_CLOCK_FREQUENCY; + } } void HLW8012Component::dump_config() { ESP_LOGCONFIG(TAG, "HLW8012:"); @@ -28,6 +45,7 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_) LOG_SENSOR(" ", "Current", this->current_sensor_) LOG_SENSOR(" ", "Power", this->power_sensor_) + LOG_SENSOR(" ", "Energy", this->energy_sensor_) } float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { @@ -49,25 +67,18 @@ void HLW8012Component::update() { return; } - const float v_ref_squared = HLW8012_REFERENCE_VOLTAGE * HLW8012_REFERENCE_VOLTAGE; - const float power_multiplier_micros = - 64000000.0f * v_ref_squared * this->voltage_divider_ / this->current_resistor_ / 24.0f / HLW8012_CLOCK_FREQUENCY; - float power = cf_hz * power_multiplier_micros / 1000000.0f; + float power = cf_hz * this->power_multiplier_; if (this->change_mode_at_ != 0) { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { - const float current_multiplier_micros = - 512000000.0f * HLW8012_REFERENCE_VOLTAGE / this->current_resistor_ / 24.0f / HLW8012_CLOCK_FREQUENCY; - float current = cf1_hz * current_multiplier_micros / 1000000.0f; + float current = cf1_hz * this->current_multiplier_; ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current); if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); } } else { - const float voltage_multiplier_micros = - 256000000.0f * HLW8012_REFERENCE_VOLTAGE * this->voltage_divider_ / HLW8012_CLOCK_FREQUENCY; - float voltage = cf1_hz * voltage_multiplier_micros / 1000000.0f; + float voltage = cf1_hz * this->voltage_multiplier_; ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); @@ -81,7 +92,7 @@ void HLW8012Component::update() { if (this->energy_sensor_ != nullptr) { cf_total_pulses_ += raw_cf; - float energy = cf_total_pulses_ * power_multiplier_micros / 3600 / 1000000.0f; + float energy = cf_total_pulses_ * this->power_multiplier_ / 3600; this->energy_sensor_->publish_state(energy); } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index af1f2e9a8c..5060957cf1 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/pulse_counter/pulse_counter_sensor.h" @@ -10,6 +10,12 @@ namespace hlw8012 { enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE }; +enum HLW8012SensorModels { + HLW8012_SENSOR_MODEL_HLW8012 = 0, + HLW8012_SENSOR_MODEL_CSE7759, + HLW8012_SENSOR_MODEL_BL0937 +}; + class HLW8012Component : public PollingComponent { public: void setup() override; @@ -20,12 +26,13 @@ class HLW8012Component : public PollingComponent { void set_initial_mode(HLW8012InitialMode initial_mode) { current_mode_ = initial_mode == HLW8012_INITIAL_MODE_CURRENT; } + void set_sensor_model(HLW8012SensorModels sensor_model) { sensor_model_ = sensor_model; } void set_change_mode_every(uint32_t change_mode_every) { change_mode_every_ = change_mode_every; } void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; } void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; } void set_sel_pin(GPIOPin *sel_pin) { sel_pin_ = sel_pin; } - void set_cf_pin(GPIOPin *cf_pin) { cf_pin_ = cf_pin; } - void set_cf1_pin(GPIOPin *cf1_pin) { cf1_pin_ = cf1_pin; } + void set_cf_pin(InternalGPIOPin *cf_pin) { cf_pin_ = cf_pin; } + void set_cf1_pin(InternalGPIOPin *cf1_pin) { cf1_pin_ = cf1_pin; } 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; } @@ -38,16 +45,21 @@ class HLW8012Component : public PollingComponent { uint32_t change_mode_every_{8}; float current_resistor_{0.001}; float voltage_divider_{2351}; + HLW8012SensorModels sensor_model_{HLW8012_SENSOR_MODEL_HLW8012}; uint64_t cf_total_pulses_{0}; GPIOPin *sel_pin_; - GPIOPin *cf_pin_; + InternalGPIOPin *cf_pin_; pulse_counter::PulseCounterStorage cf_store_; - GPIOPin *cf1_pin_; + InternalGPIOPin *cf1_pin_; pulse_counter::PulseCounterStorage cf1_store_; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; + + float voltage_multiplier_{0.0f}; + float current_multiplier_{0.0f}; + float power_multiplier_{0.0f}; }; } // namespace hlw8012 diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 6454a9fcc9..033cccc3d4 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -11,15 +11,15 @@ from esphome.const import ( CONF_POWER, CONF_ENERGY, CONF_SEL_PIN, + CONF_MODEL, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -31,37 +31,54 @@ AUTO_LOAD = ["pulse_counter"] hlw8012_ns = cg.esphome_ns.namespace("hlw8012") HLW8012Component = hlw8012_ns.class_("HLW8012Component", cg.PollingComponent) HLW8012InitialMode = hlw8012_ns.enum("HLW8012InitialMode") +HLW8012SensorModels = hlw8012_ns.enum("HLW8012SensorModels") + INITIAL_MODES = { CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT, CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE, } +MODELS = { + "HLW8012": HLW8012SensorModels.HLW8012_SENSOR_MODEL_HLW8012, + "CSE7759": HLW8012SensorModels.HLW8012_SENSOR_MODEL_CSE7759, + "BL0937": HLW8012SensorModels.HLW8012_SENSOR_MODEL_BL0937, +} + CONF_CF1_PIN = "cf1_pin" CONF_CF_PIN = "cf_pin" CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(HLW8012Component), cv.Required(CONF_SEL_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CF_PIN): cv.All( - pins.internal_gpio_input_pullup_pin_schema, pins.validate_has_interrupt - ), - cv.Required(CONF_CF1_PIN): cv.All( - pins.internal_gpio_input_pullup_pin_schema, pins.validate_has_interrupt - ), + cv.Required(CONF_CF_PIN): cv.All(pins.internal_gpio_input_pullup_pin_schema), + cv.Required(CONF_CF1_PIN): cv.All(pins.internal_gpio_input_pullup_pin_schema), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + 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_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + 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_WATT_HOURS, ICON_EMPTY, 1, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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) ), @@ -99,3 +116,4 @@ async def to_code(config): 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])) diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h index f2573ff108..fb41b921d9 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -1,5 +1,8 @@ #pragma once +#ifdef USE_ARDUINO +#include + namespace esphome { namespace hm3301 { @@ -10,3 +13,5 @@ class AbstractAQICalculator { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 627ee686fc..1410eac72b 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "abstract_aqi_calculator.h" namespace esphome { @@ -46,3 +48,5 @@ class AQICalculator : public AbstractAQICalculator { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h index 55608b6e51..3c6f9709b6 100644 --- a/esphome/components/hm3301/aqi_calculator_factory.h +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "caqi_calculator.h" #include "aqi_calculator.h" @@ -27,3 +29,5 @@ class AQICalculatorFactory { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 403bac2713..51158454d0 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/log.h" #include "abstract_aqi_calculator.h" @@ -52,3 +54,5 @@ class CAQICalculator : public AbstractAQICalculator { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 5612867d1b..759157f330 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "esphome/core/log.h" #include "hm3301.h" @@ -12,7 +14,7 @@ static const uint8_t PM_10_0_VALUE_INDEX = 7; void HM3301Component::setup() { ESP_LOGCONFIG(TAG, "Setting up HM3301..."); - hm3301_ = new HM330X(); + hm3301_ = make_unique(); error_code_ = hm3301_->init(); if (error_code_ != NO_ERROR) { this->mark_failed(); @@ -102,3 +104,5 @@ uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 5594f1719c..61bbf7e4ab 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -27,7 +29,7 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - HM330X *hm3301_; + std::unique_ptr hm3301_; HM330XErrorCode error_code_{NO_ERROR}; @@ -48,3 +50,5 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { } // namespace hm3301 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 48a29ed5f8..976a0488e1 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -6,7 +6,10 @@ from esphome.const import ( CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -43,32 +46,32 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(HM3301Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_AQI): sensor.sensor_schema( - UNIT_INDEX, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_INDEX, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_CALCULATION_TYPE): cv.enum( @@ -81,6 +84,7 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x40)), _validate, + cv.only_with_arduino, ) @@ -107,4 +111,4 @@ async def to_code(config): cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 - cg.add_library("6306", "1.0.3") + cg.add_library("seeed-studio/Grove - Laser PM2.5 Sensor HM3301", "1.0.3") diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 65469003ed..73e7472dcf 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -80,10 +79,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 0dadb78b73..cf29db8bb8 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -5,10 +5,7 @@ from esphome.const import ( CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import homeassistant_ns @@ -19,7 +16,8 @@ HomeassistantSensor = homeassistant_ns.class_( ) CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(HomeassistantSensor), diff --git a/esphome/components/hrxl_maxsonar_wr/__init__.py b/esphome/components/hrxl_maxsonar_wr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp new file mode 100644 index 0000000000..cf6c9eea65 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -0,0 +1,66 @@ +// Official Datasheet: +// https://www.maxbotix.com/documents/HRXL-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. + +#include "hrxl_maxsonar_wr.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +static const char *const TAG = "hrxl.maxsonar.wr.sensor"; +static const uint8_t ASCII_CR = 0x0D; +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. + */ +void HrxlMaxsonarWrComponent::loop() { + uint8_t data; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_ += (char) data; + this->check_buffer_(); + } + } +} + +void HrxlMaxsonarWrComponent::check_buffer_() { + // The sensor seems to inject a rogue ASCII 255 byte from time to time. Get rid of that. + if (this->buffer_.back() == static_cast(ASCII_NBSP)) { + this->buffer_.pop_back(); + return; + } + + // Stop reading at ASCII_CR. Also prevent the buffer from growing + // indefinitely if no ASCII_CR is received after MAX_DATA_LENGTH_BYTES. + 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 = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); + float meters = float(millimeters) / 1000.0; + ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); + this->publish_state(meters); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", this->buffer_.c_str()); + } + this->buffer_.clear(); + } +} + +void HrxlMaxsonarWrComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:"); + LOG_SENSOR(" ", "Distance", this); + // As specified in the sensor's data sheet + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); +} + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h new file mode 100644 index 0000000000..efb8bc5f4b --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +class HrxlMaxsonarWrComponent : 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::string buffer_; +}; + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py new file mode 100644 index 0000000000..dd43bd84a7 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, +) + +CODEOWNERS = ["@netmikey"] +DEPENDENCIES = ["uart"] + +hrxlmaxsonarwr_ns = cg.esphome_ns.namespace("hrxl_maxsonar_wr") +HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( + "HrxlMaxsonarWrComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(HrxlMaxsonarWrComponent), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 650602150a..6e249c4247 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -6,15 +6,12 @@ from esphome import automation from esphome.const import ( CONF_ID, CONF_TIMEOUT, - CONF_ESPHOME, CONF_METHOD, - CONF_ARDUINO_VERSION, - ARDUINO_VERSION_ESP8266, CONF_TRIGGER_ID, CONF_URL, + CONF_ESP8266_DISABLE_SSL_SUPPORT, ) -from esphome.core import CORE, Lambda -from esphome.core.config import PLATFORMIO_ESP8266_LUT +from esphome.core import Lambda, CORE DEPENDENCIES = ["network"] AUTO_LOAD = ["json"] @@ -36,29 +33,6 @@ CONF_VERIFY_SSL = "verify_ssl" CONF_ON_RESPONSE = "on_response" -def validate_framework(config): - if CORE.is_esp32: - return config - - version = "RECOMMENDED" - if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]: - version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION] - - if version in ["LATEST", "DEV"]: - return config - - framework = ( - PLATFORMIO_ESP8266_LUT[version] - if version in PLATFORMIO_ESP8266_LUT - else version - ) - if framework < ARDUINO_VERSION_ESP8266["2.5.1"]: - raise cv.Invalid( - "This component is not supported on arduino framework version below 2.5.1" - ) - return config - - def validate_url(value): value = cv.string(value) try: @@ -92,7 +66,7 @@ def validate_secure_url(config): return config -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(HttpRequestComponent), @@ -100,10 +74,15 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_TIMEOUT, default="5s" ): cv.positive_time_period_milliseconds, + cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( + cv.only_on_esp8266, cv.boolean + ), } - ) - .add_extra(validate_framework) - .extend(cv.COMPONENT_SCHEMA) + ).extend(cv.COMPONENT_SCHEMA), + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 5, 1), + esp32_arduino=cv.Version(0, 0, 0), + ), ) @@ -111,6 +90,13 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_useragent(config[CONF_USERAGENT])) + if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: + cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + + if CORE.is_esp32: + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) + await cg.register_component(var, config) diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 0fafa8cd86..309977a915 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -1,5 +1,9 @@ +#ifdef USE_ARDUINO + #include "http_request.h" +#include "esphome/core/macros.h" #include "esphome/core/log.h" +#include "esphome/components/network/util.h" namespace esphome { namespace http_request { @@ -25,17 +29,28 @@ void HttpRequestComponent::set_url(std::string url) { } 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(); -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 begin_status = this->client_.begin(url); #endif -#ifdef ARDUINO_ARCH_ESP8266 -#ifndef CLANG_TIDY +#ifdef USE_ESP8266 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) + this->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); +#elif ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) this->client_.setFollowRedirects(true); - this->client_.setRedirectLimit(3); - begin_status = this->client_.begin(*this->get_wifi_client_(), url); #endif +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) + this->client_.setRedirectLimit(3); +#endif + begin_status = this->client_.begin(*this->get_wifi_client_(), url); #endif if (!begin_status) { @@ -74,19 +89,21 @@ void HttpRequestComponent::send(const std::vector ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code); } -#ifdef ARDUINO_ARCH_ESP8266 -WiFiClient *HttpRequestComponent::get_wifi_client_() { +#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_ = new BearSSL::WiFiClientSecure(); + 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_ = new WiFiClient(); + this->wifi_client_ = std::make_shared(); } return this->wifi_client_; } @@ -104,3 +121,5 @@ const char *HttpRequestComponent::get_string() { } // 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 27919fe75d..9cc027b58d 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,19 +1,25 @@ #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 -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS #include #endif +#endif namespace esphome { namespace http_request { @@ -34,7 +40,7 @@ class HttpRequestComponent : public Component { 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_body(std::string body) { this->body_ = std::move(body); } + 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(); @@ -50,10 +56,12 @@ class HttpRequestComponent : public Component { uint16_t timeout_{5000}; std::string body_; std::list
headers_; -#ifdef ARDUINO_ARCH_ESP8266 - WiFiClient *wifi_client_{nullptr}; - BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; - WiFiClient *get_wifi_client_(); +#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 }; @@ -131,3 +139,5 @@ class HttpRequestResponseTrigger : public Trigger { } // namespace http_request } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index a954b2ad59..b53284ae3f 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -1,5 +1,6 @@ #include "htu21d.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace htu21d { @@ -35,18 +36,30 @@ void HTU21DComponent::dump_config() { } void HTU21DComponent::update() { uint16_t raw_temperature; - if (!this->read_byte_16(HTU21D_REGISTER_TEMPERATURE, &raw_temperature, 50)) { + 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; uint16_t raw_humidity; - if (!this->read_byte_16(HTU21D_REGISTER_HUMIDITY, &raw_humidity, 50)) { + 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 humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 435c5bf1bb..37422f0329 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HTU21DComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index 91c8317ee5..9fef649b03 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py index 17a4e35d5f..cd06cc770f 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -6,10 +6,8 @@ from esphome.const import ( CONF_CLK_PIN, CONF_GAIN, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_SCALE, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) hx711_ns = cg.esphome_ns.namespace("hx711") @@ -26,7 +24,9 @@ GAINS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_SCALE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_SCALE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index f43729066d..46f0abacc6 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,30 +2,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( - CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, + CONF_INPUT, + CONF_OUTPUT, CONF_SCAN, CONF_SCL, CONF_SDA, CONF_ADDRESS, CONF_I2C_ID, - CONF_MULTIPLEXER, ) -from esphome.core import coroutine_with_priority +from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") -I2CComponent = i2c_ns.class_("I2CComponent", cg.Component) +I2CBus = i2c_ns.class_("I2CBus") +ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component) +IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") -I2CMultiplexer = i2c_ns.class_("I2CMultiplexer", I2CDevice) + +CONF_SDA_PULLUP_ENABLED = "sda_pullup_enabled" +CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled" MULTI_CONF = True + + +def _bus_declare_type(value): + if CORE.using_arduino: + return cv.declare_id(ArduinoI2CBus)(value) + if CORE.using_esp_idf: + return cv.declare_id(IDFI2CBus)(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}), +) + + CONFIG_SCHEMA = cv.Schema( { - cv.GenerateID(): cv.declare_id(I2CComponent), - cv.Optional(CONF_SDA, default="SDA"): pins.input_pin, - cv.Optional(CONF_SCL, default="SCL"): pins.input_pin, + 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) ), @@ -33,13 +59,6 @@ CONFIG_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) -I2CMULTIPLEXER_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.use_id(I2CMultiplexer), - cv.Required(CONF_CHANNEL): cv.uint8_t, - } -) - @coroutine_with_priority(1.0) async def to_code(config): @@ -48,10 +67,16 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_sda_pin(config[CONF_SDA])) + if CONF_SDA_PULLUP_ENABLED in config: + cg.add(var.set_sda_pullup_enabled(config[CONF_SDA_PULLUP_ENABLED])) cg.add(var.set_scl_pin(config[CONF_SCL])) + if CONF_SCL_PULLUP_ENABLED in config: + cg.add(var.set_scl_pullup_enabled(config[CONF_SCL_PULLUP_ENABLED])) + cg.add(var.set_frequency(int(config[CONF_FREQUENCY]))) cg.add(var.set_scan(config[CONF_SCAN])) - cg.add_library("Wire", None) + if CORE.using_arduino: + cg.add_library("Wire", None) def i2c_device_schema(default_address): @@ -62,8 +87,11 @@ def i2c_device_schema(default_address): :return: The i2c device schema, `extend` this in your config schema. """ schema = { - cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CComponent), - cv.Optional(CONF_MULTIPLEXER): I2CMULTIPLEXER_SCHEMA, + cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), + cv.Optional("multiplexer"): cv.invalid( + "This option has been removed, please see " + "the tca9584a docs for the updated way to use multiplexers" + ), } if default_address is None: schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address @@ -80,10 +108,5 @@ async def register_i2c_device(var, config): This is a coroutine, you need to await it with a 'yield' expression! """ parent = await cg.get_variable(config[CONF_I2C_ID]) - cg.add(var.set_i2c_parent(parent)) + cg.add(var.set_i2c_bus(parent)) cg.add(var.set_i2c_address(config[CONF_ADDRESS])) - if CONF_MULTIPLEXER in config: - multiplexer = await cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID]) - cg.add( - var.set_i2c_multiplexer(multiplexer, config[CONF_MULTIPLEXER][CONF_CHANNEL]) - ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 77dfcedc04..82ab7bd09a 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,306 +1,40 @@ #include "i2c.h" #include "esphome/core/log.h" -#include "esphome/core/helpers.h" -#include "esphome/core/application.h" +#include namespace esphome { namespace i2c { static const char *const TAG = "i2c"; -I2CComponent::I2CComponent() { -#ifdef ARDUINO_ARCH_ESP32 - if (next_i2c_bus_num_ == 0) - this->wire_ = &Wire; - else - this->wire_ = new TwoWire(next_i2c_bus_num_); - next_i2c_bus_num_++; -#else - this->wire_ = &Wire; -#endif +bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { + // we have to copy in order to be able to change byte order + std::unique_ptr temp{new uint16_t[len]}; + for (size_t i = 0; i < len; i++) + temp[i] = htoi2cs(data[i]); + return write_register(a_register, reinterpret_cast(temp.get()), len * 2) == ERROR_OK; } -void I2CComponent::setup() { - this->wire_->begin(this->sda_pin_, this->scl_pin_); - this->wire_->setClock(this->frequency_); -} -void I2CComponent::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 (this->scan_) { - ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); - uint8_t found = 0; - for (uint8_t address = 1; address < 120; address++) { - this->wire_->beginTransmission(address); - uint8_t error = this->wire_->endTransmission(); - - if (error == 0) { - ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); - found++; - } else if (error == 4) { - ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); - } - - delay(1); - } - if (found == 0) { - ESP_LOGI(TAG, "Found no i2c devices!"); - } - } -} -float I2CComponent::get_setup_priority() const { return setup_priority::BUS; } - -void I2CComponent::raw_begin_transmission(uint8_t address) { - ESP_LOGVV(TAG, "Beginning Transmission to 0x%02X:", address); - this->wire_->beginTransmission(address); -} -bool I2CComponent::raw_end_transmission(uint8_t address, bool send_stop) { - uint8_t status = this->wire_->endTransmission(send_stop); - ESP_LOGVV(TAG, " Transmission ended. Status code: 0x%02X", status); - - switch (status) { - case 0: - break; - case 1: - ESP_LOGW(TAG, "Too much data to fit in transmitter buffer for address 0x%02X", address); - break; - case 2: - ESP_LOGW(TAG, "Received NACK on transmit of address 0x%02X", address); - break; - case 3: - ESP_LOGW(TAG, "Received NACK on transmit of data for address 0x%02X", address); - break; - default: - ESP_LOGW(TAG, "Unknown transmit error %u for address 0x%02X", status, address); - break; - } - - return status == 0; -} -bool I2CComponent::raw_request_from(uint8_t address, uint8_t len) { - ESP_LOGVV(TAG, "Requesting %u bytes from 0x%02X:", len, address); - uint8_t ret = this->wire_->requestFrom(address, len); - if (ret != len) { - ESP_LOGW(TAG, "Requesting %u bytes from 0x%02X failed!", len, address); - return false; - } - return true; -} -void HOT I2CComponent::raw_write(uint8_t address, const uint8_t *data, uint8_t len) { - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Writing 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - this->wire_->write(data[i]); - App.feed_wdt(); - } -} -void HOT I2CComponent::raw_write_16(uint8_t address, const uint16_t *data, uint8_t len) { - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Writing 0b" BYTE_TO_BINARY_PATTERN BYTE_TO_BINARY_PATTERN " (0x%04X)", - BYTE_TO_BINARY(data[i] >> 8), BYTE_TO_BINARY(data[i]), data[i]); - this->wire_->write(data[i] >> 8); - this->wire_->write(data[i]); - App.feed_wdt(); - } -} - -bool I2CComponent::raw_receive(uint8_t address, uint8_t *data, uint8_t len) { - if (!this->raw_request_from(address, len)) - return false; - for (uint8_t i = 0; i < len; i++) { - data[i] = this->wire_->read(); - ESP_LOGVV(TAG, " Received 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - App.feed_wdt(); - } - return true; -} -bool I2CComponent::raw_receive_16(uint8_t address, uint16_t *data, uint8_t len) { - if (!this->raw_request_from(address, len * 2)) - return false; - auto *data_8 = reinterpret_cast(data); - for (uint8_t i = 0; i < len; i++) { - data_8[i * 2 + 1] = this->wire_->read(); - data_8[i * 2] = this->wire_->read(); - ESP_LOGVV(TAG, " Received 0b" BYTE_TO_BINARY_PATTERN BYTE_TO_BINARY_PATTERN " (0x%04X)", - BYTE_TO_BINARY(data_8[i * 2 + 1]), BYTE_TO_BINARY(data_8[i * 2]), data[i]); - } - return true; -} -bool I2CComponent::read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { - if (!this->write_bytes(address, a_register, nullptr, 0)) - return false; - - if (conversion > 0) - delay(conversion); - return this->raw_receive(address, data, len); -} -bool I2CComponent::read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len) { - return this->raw_receive(address, data, len); -} -bool I2CComponent::read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len, - uint32_t conversion) { - if (!this->write_bytes(address, a_register, nullptr, 0)) - return false; - - if (conversion > 0) - delay(conversion); - return this->raw_receive_16(address, data, len); -} -bool I2CComponent::read_byte(uint8_t address, uint8_t a_register, uint8_t *data, uint32_t conversion) { - return this->read_bytes(address, a_register, data, 1, conversion); -} -bool I2CComponent::read_byte_16(uint8_t address, uint8_t a_register, uint16_t *data, uint32_t conversion) { - return this->read_bytes_16(address, a_register, data, 1, conversion); -} -bool I2CComponent::write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len) { - this->raw_begin_transmission(address); - this->raw_write(address, &a_register, 1); - this->raw_write(address, data, len); - return this->raw_end_transmission(address); -} -bool I2CComponent::write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len) { - this->raw_begin_transmission(address); - this->raw_write(address, data, len); - return this->raw_end_transmission(address); -} -bool I2CComponent::write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len) { - this->raw_begin_transmission(address); - this->raw_write(address, &a_register, 1); - this->raw_write_16(address, data, len); - return this->raw_end_transmission(address); -} -bool I2CComponent::write_byte(uint8_t address, uint8_t a_register, uint8_t data) { - return this->write_bytes(address, a_register, &data, 1); -} -bool I2CComponent::write_byte_16(uint8_t address, uint8_t a_register, uint16_t data) { - return this->write_bytes_16(address, a_register, &data, 1); -} - -void I2CDevice::set_i2c_address(uint8_t address) { this->address_ = address; } -#ifdef USE_I2C_MULTIPLEXER -void I2CDevice::set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel) { - ESP_LOGVV(TAG, " Setting Multiplexer %p for channel %d", multiplexer, channel); - this->multiplexer_ = multiplexer; - this->channel_ = channel; -} - -void I2CDevice::check_multiplexer_() { - if (this->multiplexer_ != nullptr) { - ESP_LOGVV(TAG, "Multiplexer setting channel to %d", this->channel_); - this->multiplexer_->set_channel(this->channel_); - } -} -#endif - -void I2CDevice::raw_begin_transmission() { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - this->parent_->raw_begin_transmission(this->address_); -} -bool I2CDevice::raw_end_transmission(bool send_stop) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->raw_end_transmission(this->address_, send_stop); -} -void I2CDevice::raw_write(const uint8_t *data, uint8_t len) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - this->parent_->raw_write(this->address_, data, len); -} -bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->read_bytes(this->address_, a_register, data, len, conversion); -} -bool I2CDevice::read_bytes_raw(uint8_t *data, uint8_t len) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->read_bytes_raw(this->address_, data, len); -} -bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->read_byte(this->address_, a_register, data, conversion); -} -bool I2CDevice::write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->write_bytes(this->address_, a_register, data, len); -} -bool I2CDevice::write_bytes_raw(const uint8_t *data, uint8_t len) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->write_bytes_raw(this->address_, data, len); -} -bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->write_byte(this->address_, a_register, data); -} -bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->read_bytes_16(this->address_, a_register, data, len, conversion); -} -bool I2CDevice::read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->read_byte_16(this->address_, a_register, data, conversion); -} -bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->write_bytes_16(this->address_, a_register, data, len); -} -bool I2CDevice::write_byte_16(uint8_t a_register, uint16_t data) { // NOLINT -#ifdef USE_I2C_MULTIPLEXER - this->check_multiplexer_(); -#endif - return this->parent_->write_byte_16(this->address_, a_register, data); -} -void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } - -#ifdef ARDUINO_ARCH_ESP32 -uint8_t next_i2c_bus_num_ = 0; -#endif - I2CRegister &I2CRegister::operator=(uint8_t value) { - this->parent_->write_byte(this->register_, value); + this->parent_->write_register(this->register_, &value, 1); return *this; } - I2CRegister &I2CRegister::operator&=(uint8_t value) { - this->parent_->write_byte(this->register_, this->get() & value); + value &= get(); + this->parent_->write_register(this->register_, &value, 1); return *this; } - I2CRegister &I2CRegister::operator|=(uint8_t value) { - this->parent_->write_byte(this->register_, this->get() | value); + value |= get(); + this->parent_->write_register(this->register_, &value, 1); return *this; } -uint8_t I2CRegister::get() { +uint8_t I2CRegister::get() const { uint8_t value = 0x00; - this->parent_->read_byte(this->register_, &value); + this->parent_->read_register(this->register_, &value, 1); return value; } -I2CRegister &I2CRegister::operator=(const std::vector &value) { - this->parent_->write_bytes(this->register_, value); - return *this; -} } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index da791ec633..7ee4cdd811 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,202 +1,81 @@ #pragma once -#include -#include "esphome/core/defines.h" -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" +#include "i2c_bus.h" +#include "esphome/core/optional.h" +#include +#include namespace esphome { namespace i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); -/** The I2CComponent is the base of ESPHome's i2c communication. - * - * It handles setting up the bus (with pins, clock frequency) and provides nice helper functions to - * make reading from the i2c bus easier (see read_bytes, write_bytes) and safe (with read timeouts). - * - * For the user, it has a few setters (see set_sda_pin, set_scl_pin, set_frequency) - * to setup some parameters for the bus. Additionally, the i2c component has a scan feature that will - * scan the entire 7-bit i2c address range for devices that respond to transmissions to make finding - * the address of an i2c device easier. - * - * On the ESP32, you can even have multiple I2C bus for communication, simply create multiple - * I2CComponents, each with different SDA and SCL pins and use `set_parent` on all I2CDevices that use - * the non-first I2C bus. - */ -class I2CComponent : public Component { - public: - I2CComponent(); - 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_scan(bool scan) { scan_ = scan; } - - /** Read len amount of bytes from a register into data. Optionally with a conversion time after - * writing the register value to the bus. - * - * @param address The address to send the request to. - * @param a_register The register number to write to the bus before reading. - * @param data An array to store len amount of 8-bit bytes into. - * @param len The amount of bytes to request and write into data. - * @param conversion The time in ms between writing the register value and reading out the value. - * @return If the operation was successful. - */ - bool read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); - bool read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len); - - /** Read len amount of 16-bit words (MSB first) from a register into data. - * - * @param address The address to send the request to. - * @param a_register The register number to write to the bus before reading. - * @param data An array to store len amount of 16-bit words into. - * @param len The amount of 16-bit words to request and write into data. - * @param conversion The time in ms between writing the register value and reading out the value. - * @return If the operation was successful. - */ - bool read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); - - /// Read a single byte from a register into the data variable. Return true if successful. - bool read_byte(uint8_t address, uint8_t a_register, uint8_t *data, uint32_t conversion = 0); - - /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. - bool read_byte_16(uint8_t address, uint8_t a_register, uint16_t *data, uint32_t conversion = 0); - - /** Write len amount of 8-bit bytes to the specified register for address. - * - * @param address The address to use for the transmission. - * @param a_register The register to write the values to. - * @param data An array from which len bytes of data will be written to the bus. - * @param len The amount of bytes to write to the bus. - * @return If the operation was successful. - */ - bool write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len); - bool write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len); - - /** Write len amount of 16-bit words (MSB first) to the specified register for address. - * - * @param address The address to use for the transmission. - * @param a_register The register to write the values to. - * @param data An array from which len 16-bit words of data will be written to the bus. - * @param len The amount of bytes to write to the bus. - * @return If the operation was successful. - */ - bool write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len); - - /// Write a single byte of data into the specified register of address. Return true if successful. - bool write_byte(uint8_t address, uint8_t a_register, uint8_t data); - - /// Write a single 16-bit word of data into the specified register of address. Return true if successful. - bool write_byte_16(uint8_t address, uint8_t a_register, uint16_t data); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - /// Begin a write transmission to an address. - void raw_begin_transmission(uint8_t address); - - /// End a write transmission to an address, return true if successful. - bool raw_end_transmission(uint8_t address, bool send_stop = true); - - /** Request data from an address with a number of (8-bit) bytes. - * - * @param address The address to request the bytes from. - * @param len The number of bytes to receive, must not be 0. - * @return True if all requested bytes were read, false otherwise. - */ - bool raw_request_from(uint8_t address, uint8_t len); - - /// Write len amount of bytes from data to address. begin_transmission_ must be called before this. - void raw_write(uint8_t address, const uint8_t *data, uint8_t len); - - /// Write len amount of 16-bit words from data to address. begin_transmission_ must be called before this. - void raw_write_16(uint8_t address, const uint16_t *data, uint8_t len); - - /// Request len amount of bytes from address and write the result it into data. Returns true iff was successful. - bool raw_receive(uint8_t address, uint8_t *data, uint8_t len); - - /// Request len amount of 16-bit words from address and write the result into data. Returns true iff was successful. - bool raw_receive_16(uint8_t address, uint16_t *data, uint8_t len); - - /// Setup the i2c. bus - void setup() override; - void dump_config() override; - /// Set a very high setup priority to make sure it's loaded before all other hardware. - float get_setup_priority() const override; - - protected: - TwoWire *wire_; - uint8_t sda_pin_; - uint8_t scl_pin_; - uint32_t frequency_; - bool scan_; -}; - -#ifdef ARDUINO_ARCH_ESP32 -extern uint8_t next_i2c_bus_num_; -#endif - class I2CDevice; -class I2CMultiplexer; class I2CRegister { public: - I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} - I2CRegister &operator=(uint8_t value); - I2CRegister &operator=(const std::vector &value); I2CRegister &operator&=(uint8_t value); I2CRegister &operator|=(uint8_t value); - uint8_t get(); + explicit operator uint8_t() const { return get(); } + + uint8_t get() const; protected: + friend class I2CDevice; + + I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} + I2CDevice *parent_; uint8_t register_; }; -/** All components doing communication on the I2C bus should subclass I2CDevice. - * - * This class stores 1. the address of the i2c device and has a helper function to allow - * users to manually set the address and 2. stores a reference to the "parent" I2CComponent. - * - * - * All this class basically does is to expose all helper functions from I2CComponent. - */ +// like ntohs/htons but without including networking headers. +// ("i2c" byte order is big-endian) +inline uint16_t i2ctohs(uint16_t i2cshort) { + union { + uint16_t x; + uint8_t y[2]; + } conv; + conv.x = i2cshort; + return ((uint16_t) conv.y[0] << 8) | ((uint16_t) conv.y[1] << 0); +} + +inline uint16_t htoi2cs(uint16_t hostshort) { return i2ctohs(hostshort); } + class I2CDevice { public: I2CDevice() = default; - I2CDevice(I2CComponent *parent, uint8_t address) : address_(address), parent_(parent) {} - /// Manually set the i2c address of this device. - void set_i2c_address(uint8_t address); -#ifdef USE_I2C_MULTIPLEXER - /// Manually set the i2c multiplexer of this device. - void set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel); -#endif - /// Manually set the parent i2c bus for this device. - void set_i2c_parent(I2CComponent *parent); + void set_i2c_address(uint8_t address) { address_ = address; } + void set_i2c_bus(I2CBus *bus) { bus_ = bus; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; } - /// Begin a write transmission. - void raw_begin_transmission(); + ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len) { + ErrorCode err = this->write(&a_register, 1); + if (err != ERROR_OK) + return err; + return this->read(data, len); + } - /// End a write transmission, return true if successful. - bool raw_end_transmission(bool send_stop = true); + ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); } + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) { + WriteBuffer buffers[2]; + buffers[0].data = &a_register; + buffers[0].len = 1; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2); + } - /// Write len amount of bytes from data. begin_transmission_ must be called before this. - void raw_write(const uint8_t *data, uint8_t len); + // Compat APIs - /** Read len amount of bytes from a register into data. Optionally with a conversion time after - * writing the register value to the bus. - * - * @param a_register The register number to write to the bus before reading. - * @param data An array to store len amount of 8-bit bytes into. - * @param len The amount of bytes to request and write into data. - * @param conversion The time in ms between writing the register value and reading out the value. - * @return If the operation was successful. - */ - bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); - bool read_bytes_raw(uint8_t *data, uint8_t len); + 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) { std::array res; @@ -213,18 +92,15 @@ class I2CDevice { return res; } - /** Read len amount of 16-bit words (MSB first) from a register into data. - * - * @param a_register The register number to write to the bus before reading. - * @param data An array to store len amount of 16-bit words into. - * @param len The amount of 16-bit words to request and write into data. - * @param conversion The time in ms between writing the register value and reading out the value. - * @return If the operation was successful. - */ - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); + bool 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; + for (size_t i = 0; i < len; i++) + data[i] = i2ctohs(data[i]); + return true; + } - /// Read a single byte from a register into the data variable. Return true if successful. - bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); + bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } optional read_byte(uint8_t a_register) { uint8_t data; @@ -233,66 +109,30 @@ class I2CDevice { return data; } - /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. - bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); + bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - /** Write len amount of 8-bit bytes to the specified register. - * - * @param a_register The register to write the values to. - * @param data An array from which len bytes of data will be written to the bus. - * @param len The amount of bytes to write to the bus. - * @return If the operation was successful. - */ - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); - bool write_bytes_raw(const uint8_t *data, uint8_t len); - - /** Write a vector of data to a register. - * - * @param a_register The register to write to. - * @param data The data to write. - * @return If the operation was successful. - */ - bool write_bytes(uint8_t a_register, const std::vector &data) { - return this->write_bytes(a_register, data.data(), data.size()); + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { + return write_register(a_register, data, len) == ERROR_OK; + } + + bool write_bytes(uint8_t a_register, const std::vector &data) { + return write_bytes(a_register, data.data(), data.size()); } - bool write_bytes_raw(const std::vector &data) { return this->write_bytes_raw(data.data(), data.size()); } template bool write_bytes(uint8_t a_register, const std::array &data) { - return this->write_bytes(a_register, data.data(), data.size()); - } - template bool write_bytes_raw(const std::array &data) { - return this->write_bytes_raw(data.data(), data.size()); + return write_bytes(a_register, data.data(), data.size()); } - /** Write len amount of 16-bit words (MSB first) to the specified register. - * - * @param a_register The register to write the values to. - * @param data An array from which len 16-bit words of data will be written to the bus. - * @param len The amount of bytes to write to the bus. - * @return If the operation was successful. - */ bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); - /// Write a single byte of data into the specified register. Return true if successful. - bool write_byte(uint8_t a_register, uint8_t data); + bool write_byte(uint8_t a_register, uint8_t data) { return write_bytes(a_register, &data, 1); } - /// Write a single 16-bit word of data into the specified register. Return true if successful. - bool write_byte_16(uint8_t a_register, uint16_t data); + bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } protected: - // Checks for multiplexer set and set channel - void check_multiplexer_(); uint8_t address_{0x00}; - I2CComponent *parent_{nullptr}; -#ifdef USE_I2C_MULTIPLEXER - I2CMultiplexer *multiplexer_{nullptr}; - uint8_t channel_; -#endif -}; -class I2CMultiplexer : public I2CDevice { - public: - I2CMultiplexer() = default; - virtual void set_channel(uint8_t channelno); + I2CBus *bus_{nullptr}; }; + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h new file mode 100644 index 0000000000..cb00260f43 --- /dev/null +++ b/esphome/components/i2c/i2c_bus.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +namespace esphome { +namespace i2c { + +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, +}; + +struct ReadBuffer { + uint8_t *data; + size_t len; +}; +struct WriteBuffer { + const uint8_t *data; + size_t len; +}; + +class I2CBus { + public: + 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; + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { + WriteBuffer buf; + buf.data = buffer; + buf.len = len; + return writev(address, &buf, 1); + } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0; +}; + +} // namespace i2c +} // namespace esphome diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp new file mode 100644 index 0000000000..4afabbfa53 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -0,0 +1,260 @@ +#ifdef USE_ARDUINO + +#include "i2c_bus_arduino.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace i2c { + +static const char *const TAG = "i2c.arduino"; + +void ArduinoI2CBus::setup() { + recover_(); + +#ifdef USE_ESP32 + static uint8_t next_bus_num = 0; + if (next_bus_num == 0) + wire_ = &Wire; + else + wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) + next_bus_num++; +#else + wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) +#endif + + wire_->begin(sda_pin_, scl_pin_); + wire_->setClock(frequency_); + initialized_ = true; +} +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_); + switch (this->recovery_result_) { + case RECOVERY_COMPLETED: + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + break; + case RECOVERY_FAILED_SCL_LOW: + ESP_LOGCONFIG(TAG, " Recovery: failed, SCL is held low on the bus"); + break; + case RECOVERY_FAILED_SDA_LOW: + ESP_LOGCONFIG(TAG, " Recovery: failed, SDA is held low on the bus"); + break; + } + if (this->scan_) { + ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); + uint8_t found = 0; + for (uint8_t address = 8; address < 120; address++) { + auto err = writev(address, nullptr, 0); + if (err == ERROR_OK) { + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); + found++; + } else if (err == ERROR_UNKNOWN) { + ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); + } + } + if (found == 0) { + ESP_LOGI(TAG, "Found no i2c devices!"); + } + } +} +ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { + // logging is only enabled with vv level, if warnings are shown the caller + // should log them + if (!initialized_) { + ESP_LOGVV(TAG, "i2c bus not initialized!"); + return ERROR_NOT_INITIALIZED; + } + size_t to_request = 0; + for (size_t i = 0; i < cnt; i++) + to_request += buffers[i].len; + size_t ret = wire_->requestFrom((int) address, (int) to_request, 1); + if (ret != to_request) { + ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); + return ERROR_TIMEOUT; + } + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + for (size_t j = 0; j < buf.len; j++) + buf.data[j] = wire_->read(); + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char debug_buf[4]; + std::string debug_hex; + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + for (size_t j = 0; j < buf.len; j++) { + snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); + debug_hex += debug_buf; + } + } + ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); +#endif + + return ERROR_OK; +} +ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { + // logging is only enabled with vv level, if warnings are shown the caller + // should log them + if (!initialized_) { + ESP_LOGVV(TAG, "i2c bus not initialized!"); + return ERROR_NOT_INITIALIZED; + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char debug_buf[4]; + std::string debug_hex; + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + for (size_t j = 0; j < buf.len; j++) { + snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); + debug_hex += debug_buf; + } + } + ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); +#endif + + wire_->beginTransmission(address); + size_t written = 0; + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) + continue; + size_t ret = wire_->write(buf.data, buf.len); + written += ret; + if (ret != buf.len) { + ESP_LOGVV(TAG, "TX failed at %u", written); + return ERROR_UNKNOWN; + } + } + uint8_t status = wire_->endTransmission(true); + if (status == 0) { + return ERROR_OK; + } else if (status == 1) { + // transmit buffer not large enough + ESP_LOGVV(TAG, "TX failed: buffer not large enough"); + return ERROR_UNKNOWN; + } else if (status == 2 || status == 3) { + ESP_LOGVV(TAG, "TX failed: not acknowledged"); + return ERROR_NOT_ACKNOWLEDGED; + } + ESP_LOGVV(TAG, "TX failed: unknown error %u", status); + return ERROR_UNKNOWN; +} + +/// Perform I2C bus recovery, see: +/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf +/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf +void ArduinoI2CBus::recover_() { + ESP_LOGI(TAG, "Performing I2C bus recovery"); + + // For the upcoming operations, target for a 100kHz toggle frequency. + // This is the maximum frequency for I2C running in standard-mode. + // The actual frequency will be lower, because of the additional + // function calls that are done, but that is no problem. + const auto half_period_usec = 1000000 / 100000 / 2; + + // Activate input and pull up resistor for the SCL pin. + pinMode(scl_pin_, INPUT_PULLUP); // NOLINT + + // This should make the signal on the line HIGH. If SCL is pulled low + // on the I2C bus however, then some device is interfering with the SCL + // line. In that case, the I2C bus cannot be recovered. + delayMicroseconds(half_period_usec); + if (digitalRead(scl_pin_) == LOW) { // NOLINT + ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the I2C bus"); + recovery_result_ = RECOVERY_FAILED_SCL_LOW; + return; + } + + // From the specification: + // "If the data line (SDA) is stuck LOW, send nine clock pulses. The + // device that held the bus LOW should release it sometime within + // those nine clocks." + // We don't really have to detect if SDA is stuck low. We'll simply send + // nine clock pulses here, just in case SDA is stuck. Actual checks on + // the SDA line status will be done after the clock pulses. + + // Make sure that switching to output mode will make SCL low, just in + // case other code has setup the pin for a HIGH signal. + digitalWrite(scl_pin_, LOW); // NOLINT + + delayMicroseconds(half_period_usec); + for (auto i = 0; i < 9; i++) { + // Release pull up resistor and switch to output to make the signal LOW. + pinMode(scl_pin_, INPUT); // NOLINT + pinMode(scl_pin_, OUTPUT); // NOLINT + delayMicroseconds(half_period_usec); + + // Release output and activate pull up resistor to make the signal HIGH. + pinMode(scl_pin_, INPUT); // NOLINT + pinMode(scl_pin_, INPUT_PULLUP); // NOLINT + delayMicroseconds(half_period_usec); + + // When SCL is kept LOW at this point, we might be looking at a device + // that applies clock stretching. Wait for the release of the SCL line, + // but not forever. There is no specification for the maximum allowed + // time. We'll stick to 500ms here. + auto wait = 20; + while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT + delay(25); + } + if (digitalRead(scl_pin_) == LOW) { // NOLINT + ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); + recovery_result_ = RECOVERY_FAILED_SCL_LOW; + return; + } + } + + // Activate input and pull resistor for the SDA pin, so we can verify + // that SDA is pulled HIGH in the following step. + pinMode(sda_pin_, INPUT_PULLUP); // NOLINT + digitalWrite(sda_pin_, LOW); // NOLINT + + // By now, any stuck device ought to have sent all remaining bits of its + // transation, meaning that it should have freed up the SDA line, resulting + // in SDA being pulled up. + if (digitalRead(sda_pin_) == LOW) { // NOLINT + ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); + recovery_result_ = RECOVERY_FAILED_SDA_LOW; + return; + } + + // From the specification: + // "I2C-bus compatible devices must reset their bus logic on receipt of + // a START or repeated START condition such that they all anticipate + // the sending of a target address, even if these START conditions are + // not positioned according to the proper format." + // While the 9 clock pulses from above might have drained all bits of a + // single byte within a transaction, a device might have more bytes to + // transmit. So here we'll generate a START condition to snap the device + // out of this state. + // SCL and SDA are already high at this point, so we can generate a START + // condition by making the SDA signal LOW. + delayMicroseconds(half_period_usec); + pinMode(sda_pin_, INPUT); // NOLINT + pinMode(sda_pin_, OUTPUT); // NOLINT + + // From the specification: + // "A START condition immediately followed by a STOP condition (void + // message) is an illegal format. Many devices however are designed to + // operate properly under this condition." + // Finally, we'll bring the I2C bus into a starting state by generating + // a STOP condition. + delayMicroseconds(half_period_usec); + pinMode(sda_pin_, INPUT); // NOLINT + pinMode(sda_pin_, INPUT_PULLUP); // NOLINT + + recovery_result_ = RECOVERY_COMPLETED; +} +} // namespace i2c +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h new file mode 100644 index 0000000000..82f043ef7d --- /dev/null +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -0,0 +1,47 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "i2c_bus.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace i2c { + +enum RecoveryCode { + RECOVERY_FAILED_SCL_LOW, + RECOVERY_FAILED_SDA_LOW, + RECOVERY_COMPLETED, +}; + +class ArduinoI2CBus : public I2CBus, public Component { + public: + void setup() override; + void dump_config() override; + ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + 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; } + + private: + void recover_(); + RecoveryCode recovery_result_; + + protected: + TwoWire *wire_; + bool scan_; + uint8_t sda_pin_; + uint8_t scl_pin_; + uint32_t frequency_; + bool initialized_ = false; +}; + +} // namespace i2c +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp new file mode 100644 index 0000000000..f7ecfe5f7c --- /dev/null +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -0,0 +1,322 @@ +#ifdef USE_ESP_IDF + +#include "i2c_bus_esp_idf.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace i2c { + +static const char *const TAG = "i2c.idf"; + +void IDFI2CBus::setup() { + static i2c_port_t next_port = 0; + port_ = next_port++; + + recover_(); + + i2c_config_t conf{}; + memset(&conf, 0, sizeof(conf)); + conf.mode = I2C_MODE_MASTER; + conf.sda_io_num = sda_pin_; + conf.sda_pullup_en = sda_pullup_enabled_; + conf.scl_io_num = scl_pin_; + conf.scl_pullup_en = scl_pullup_enabled_; + conf.master.clk_speed = frequency_; + esp_err_t err = i2c_param_config(port_, &conf); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + 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)); + this->mark_failed(); + return; + } + initialized_ = true; +} +void IDFI2CBus::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_); + switch (this->recovery_result_) { + case RECOVERY_COMPLETED: + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + break; + case RECOVERY_FAILED_SCL_LOW: + ESP_LOGCONFIG(TAG, " Recovery: failed, SCL is held low on the bus"); + break; + case RECOVERY_FAILED_SDA_LOW: + ESP_LOGCONFIG(TAG, " Recovery: failed, SDA is held low on the bus"); + break; + } + if (this->scan_) { + ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); + uint8_t found = 0; + for (uint8_t address = 8; address < 120; address++) { + auto err = writev(address, nullptr, 0); + + if (err == ERROR_OK) { + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); + found++; + } else if (err == ERROR_UNKNOWN) { + ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); + } + } + if (found == 0) { + ESP_LOGI(TAG, "Found no i2c devices!"); + } + } +} +ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { + // logging is only enabled with vv level, if warnings are shown the caller + // should log them + if (!initialized_) { + ESP_LOGVV(TAG, "i2c bus not initialized!"); + return ERROR_NOT_INITIALIZED; + } + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + esp_err_t err = i2c_master_start(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X master start failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } + err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, true); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X address write failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) + continue; + err = i2c_master_read(cmd, buf.data, buf.len, i == cnt - 1 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X data read failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } + } + err = i2c_master_stop(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X 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); + if (err == ESP_FAIL) { + // transfer not acked + ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); + return ERROR_NOT_ACKNOWLEDGED; + } else if (err == ESP_ERR_TIMEOUT) { + ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); + return ERROR_TIMEOUT; + } else if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); + return ERROR_UNKNOWN; + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char debug_buf[4]; + std::string debug_hex; + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + for (size_t j = 0; j < buf.len; j++) { + snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); + debug_hex += debug_buf; + } + } + ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); +#endif + + return ERROR_OK; +} +ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { + // logging is only enabled with vv level, if warnings are shown the caller + // should log them + if (!initialized_) { + ESP_LOGVV(TAG, "i2c bus not initialized!"); + return ERROR_NOT_INITIALIZED; + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char debug_buf[4]; + std::string debug_hex; + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + for (size_t j = 0; j < buf.len; j++) { + snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); + debug_hex += debug_buf; + } + } + ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); +#endif + + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + esp_err_t err = i2c_master_start(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X master start failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } + err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X address write failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) + continue; + err = i2c_master_write(cmd, buf.data, buf.len, true); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X data write failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + 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; + } + err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); + i2c_cmd_link_delete(cmd); + if (err == ESP_FAIL) { + // transfer not acked + ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); + return ERROR_NOT_ACKNOWLEDGED; + } else if (err == ESP_ERR_TIMEOUT) { + ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); + return ERROR_TIMEOUT; + } else if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); + return ERROR_UNKNOWN; + } + return ERROR_OK; +} + +/// Perform I2C bus recovery, see: +/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf +/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf +void IDFI2CBus::recover_() { + ESP_LOGI(TAG, "Performing I2C bus recovery"); + + const gpio_num_t scl_pin = static_cast(scl_pin_); + const gpio_num_t sda_pin = static_cast(sda_pin_); + + // For the upcoming operations, target for a 60kHz toggle frequency. + // 1000kHz is the maximum frequency for I2C running in standard-mode, + // but lower frequencies are not a problem. + // Note: the timing that is used here is chosen manually, to get + // results that are close to the timing that can be archieved by the + // implementation for the Arduino framework. + const auto half_period_usec = 7; + + // Configure SCL pin for open drain input/output, with a pull up resistor. + gpio_set_level(scl_pin, 1); + gpio_config_t scl_config{}; + scl_config.pin_bit_mask = 1ULL << scl_pin_; + scl_config.mode = GPIO_MODE_INPUT_OUTPUT_OD; + scl_config.pull_up_en = GPIO_PULLUP_ENABLE; + scl_config.pull_down_en = GPIO_PULLDOWN_DISABLE; + scl_config.intr_type = GPIO_INTR_DISABLE; + gpio_config(&scl_config); + + // Configure SDA pin for open drain input/output, with a pull up resistor. + gpio_set_level(sda_pin, 1); + gpio_config_t sda_conf{}; + sda_conf.pin_bit_mask = 1ULL << sda_pin_; + sda_conf.mode = GPIO_MODE_INPUT_OUTPUT_OD; + sda_conf.pull_up_en = GPIO_PULLUP_ENABLE; + sda_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + sda_conf.intr_type = GPIO_INTR_DISABLE; + gpio_config(&sda_conf); + + // If SCL is pulled low on the I2C bus, then some device is interfering + // with the SCL line. In that case, the I2C bus cannot be recovered. + delayMicroseconds(half_period_usec); + if (gpio_get_level(scl_pin) == 0) { + ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the I2C bus"); + recovery_result_ = RECOVERY_FAILED_SCL_LOW; + return; + } + + // From the specification: + // "If the data line (SDA) is stuck LOW, send nine clock pulses. The + // device that held the bus LOW should release it sometime within + // those nine clocks." + // We don't really have to detect if SDA is stuck low. We'll simply send + // nine clock pulses here, just in case SDA is stuck. Actual checks on + // the SDA line status will be done after the clock pulses. + for (auto i = 0; i < 9; i++) { + gpio_set_level(scl_pin, 0); + delayMicroseconds(half_period_usec); + gpio_set_level(scl_pin, 1); + delayMicroseconds(half_period_usec); + + // When SCL is kept LOW at this point, we might be looking at a device + // that applies clock stretching. Wait for the release of the SCL line, + // but not forever. There is no specification for the maximum allowed + // time. We'll stick to 500ms here. + auto wait = 20; + while (wait-- && gpio_get_level(scl_pin) == 0) { + delay(25); + } + if (gpio_get_level(scl_pin) == 0) { + ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); + recovery_result_ = RECOVERY_FAILED_SCL_LOW; + return; + } + } + + // By now, any stuck device ought to have sent all remaining bits of its + // transation, meaning that it should have freed up the SDA line, resulting + // in SDA being pulled up. + if (gpio_get_level(sda_pin) == 0) { + ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); + recovery_result_ = RECOVERY_FAILED_SDA_LOW; + return; + } + + // From the specification: + // "I2C-bus compatible devices must reset their bus logic on receipt of + // a START or repeated START condition such that they all anticipate + // the sending of a target address, even if these START conditions are + // not positioned according to the proper format." + // While the 9 clock pulses from above might have drained all bits of a + // single byte within a transaction, a device might have more bytes to + // transmit. So here we'll generate a START condition to snap the device + // out of this state. + // SCL and SDA are already high at this point, so we can generate a START + // condition by making the SDA signal LOW. + delayMicroseconds(half_period_usec); + gpio_set_level(sda_pin, 0); + + // From the specification: + // "A START condition immediately followed by a STOP condition (void + // message) is an illegal format. Many devices however are designed to + // operate properly under this condition." + // Finally, we'll bring the I2C bus into a starting state by generating + // a STOP condition. + delayMicroseconds(half_period_usec); + gpio_set_level(sda_pin, 1); + + recovery_result_ = RECOVERY_COMPLETED; +} + +} // namespace i2c +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h new file mode 100644 index 0000000000..13d996dbd8 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "i2c_bus.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace i2c { + +enum RecoveryCode { + RECOVERY_FAILED_SCL_LOW, + RECOVERY_FAILED_SDA_LOW, + RECOVERY_COMPLETED, +}; + +class IDFI2CBus : public I2CBus, public Component { + public: + void setup() override; + void dump_config() override; + ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } + void set_sda_pullup_enabled(bool sda_pullup_enabled) { sda_pullup_enabled_ = sda_pullup_enabled; } + 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; } + + private: + void recover_(); + RecoveryCode recovery_result_; + + protected: + i2c_port_t port_; + bool scan_; + uint8_t sda_pin_; + bool sda_pullup_enabled_; + uint8_t scl_pin_; + bool scl_pullup_enabled_; + uint32_t frequency_; + bool initialized_ = false; +}; + +} // namespace i2c +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 450a958c56..157e8212bd 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.polling_component_schema("1s")) - .extend(spi.spi_device_schema()), + .extend(spi.spi_device_schema(False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index e973671acc..ab5586fa28 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace ili9341 { @@ -92,12 +93,14 @@ void ILI9341Display::display_() { this->start_data_(); uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); for (uint16_t row = 0; row < h; row++) { - for (uint16_t col = 0; col < w; col++) { - uint32_t pos = start_pos + (row * width_) + col; + uint32_t pos = start_pos + (row * width_); + uint32_t rem = w; - uint16_t color = convert_to_16bit_color_(buffer_[pos]); - this->write_byte(color >> 8); - this->write_byte(color); + while (rem > 0) { + uint32_t sz = buffer_to_transfer_(pos, rem); + this->write_array(transfer_buffer_, 2 * sz); + pos += sz; + rem -= sz; } } this->end_data_(); @@ -139,16 +142,32 @@ void ILI9341Display::fill(Color color) { } void ILI9341Display::fill_internal_(Color color) { + if (color.raw_32 == Color::BLACK.raw_32) { + memset(transfer_buffer_, 0, sizeof(transfer_buffer_)); + } else { + uint8_t *dst = transfer_buffer_; + auto color565 = display::ColorUtil::color_to_565(color); + + while (dst < transfer_buffer_ + sizeof(transfer_buffer_)) { + *dst++ = (uint8_t)(color565 >> 8); + *dst++ = (uint8_t) color565; + } + } + + uint32_t rem = this->get_width_internal() * this->get_height_internal(); + this->set_addr_window_(0, 0, this->get_width_internal(), this->get_height_internal()); this->start_data_(); - auto color565 = display::ColorUtil::color_to_565(color); - for (uint32_t i = 0; i < (this->get_width_internal()) * (this->get_height_internal()); i++) { - this->write_byte(color565 >> 8); - this->write_byte(color565); - buffer_[i] = 0; + while (rem > 0) { + size_t sz = rem <= sizeof(transfer_buffer_) ? rem : sizeof(transfer_buffer_); + this->write_array(transfer_buffer_, sz); + rem -= sz; } + this->end_data_(); + + memset(buffer_, 0, (this->get_width_internal()) * (this->get_height_internal())); } void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) { @@ -185,8 +204,8 @@ void ILI9341Display::end_data_() { this->disable(); } void ILI9341Display::init_lcd_(const uint8_t *init_cmd) { uint8_t cmd, x, num_args; const uint8_t *addr = init_cmd; - while ((cmd = pgm_read_byte(addr++)) > 0) { - x = pgm_read_byte(addr++); + 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; @@ -219,13 +238,30 @@ void ILI9341Display::invert_display_(bool invert) { this->command(invert ? ILI93 int ILI9341Display::get_width_internal() { return this->width_; } int ILI9341Display::get_height_internal() { return this->height_; } +uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { + uint8_t *src = buffer_ + pos; + uint8_t *dst = transfer_buffer_; + + if (sz > sizeof(transfer_buffer_) / 2) { + sz = sizeof(transfer_buffer_) / 2; + } + + for (uint32_t i = 0; i < sz; ++i) { + uint16_t color = convert_to_16bit_color_(*src++); + *dst++ = (uint8_t)(color >> 8); + *dst++ = (uint8_t) color; + } + + return sz; +} + // M5Stack display void ILI9341M5Stack::initialize() { this->init_lcd_(INITCMD_M5STACK); this->width_ = 320; this->height_ = 240; this->invert_display_(true); - this->fill_internal_(COLOR_BLACK); + this->fill_internal_(Color::BLACK); } // 24_TFT display @@ -233,7 +269,7 @@ void ILI9341TFT24::initialize() { this->init_lcd_(INITCMD_TFT); this->width_ = 240; this->height_ = 320; - this->fill_internal_(COLOR_BLACK); + this->fill_internal_(Color::BLACK); } } // namespace ili9341 diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h index 2b6ecc6871..d8c90c9d33 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9341/ili9341_display.h @@ -71,6 +71,10 @@ class ILI9341Display : public PollingComponent, void start_data_(); void end_data_(); + uint8_t transfer_buffer_[64]; + + uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz); + GPIOPin *reset_pin_{nullptr}; GPIOPin *led_pin_{nullptr}; GPIOPin *dc_pin_; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index b946a86bc4..a721263dff 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -4,7 +4,14 @@ from esphome import core from esphome.components import display, font import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE, CONF_DITHER +from esphome.const import ( + CONF_DITHER, + CONF_FILE, + CONF_ID, + CONF_RAW_DATA_ID, + CONF_RESIZE, + CONF_TYPE, +) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) @@ -21,8 +28,6 @@ IMAGE_TYPE = { Image_ = display.display_ns.class_("Image") -CONF_RAW_DATA_ID = "raw_data_id" - IMAGE_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Image_), diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp index d1fee72866..4f6ed7702d 100644 --- a/esphome/components/improv/improv.cpp +++ b/esphome/components/improv/improv.cpp @@ -65,6 +65,7 @@ std::vector build_rpc_response(Command command, const std::vector build_rpc_response(Command command, const std::vector &datum) { std::vector out; uint32_t length = 0; @@ -85,5 +86,6 @@ std::vector build_rpc_response(Command command, const std::vector #include #include @@ -50,6 +53,8 @@ ImprovCommand parse_improv_data(const std::vector &data); ImprovCommand parse_improv_data(const uint8_t *data, size_t length); std::vector build_rpc_response(Command command, const std::vector &datum); +#ifdef USE_ARDUINO std::vector build_rpc_response(Command command, const std::vector &datum); +#endif // USE_ARDUINO } // namespace improv diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 506b7e06ed..609f3d0f08 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -1,5 +1,6 @@ #include "ina219.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina219 { @@ -149,7 +150,7 @@ float INA219Component::get_setup_priority() const { return setup_priority::DATA; void INA219Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; - if (!this->read_byte_16(INA219_REGISTER_BUS_VOLTAGE, &raw_bus_voltage, 1)) { + if (!this->read_byte_16(INA219_REGISTER_BUS_VOLTAGE, &raw_bus_voltage)) { this->status_set_warning(); return; } @@ -160,8 +161,9 @@ void INA219Component::update() { if (this->shunt_voltage_sensor_ != nullptr) { uint16_t raw_shunt_voltage; - if (!this->read_byte_16(INA219_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage, 1)) { + if (!this->read_byte_16(INA219_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage)) { this->status_set_warning(); + return; } float shunt_voltage_mv = int16_t(raw_shunt_voltage) * 0.01f; this->shunt_voltage_sensor_->publish_state(shunt_voltage_mv / 1000.0f); @@ -169,7 +171,7 @@ void INA219Component::update() { if (this->current_sensor_ != nullptr) { uint16_t raw_current; - if (!this->read_byte_16(INA219_REGISTER_CURRENT, &raw_current, 1)) { + if (!this->read_byte_16(INA219_REGISTER_CURRENT, &raw_current)) { this->status_set_warning(); return; } @@ -179,7 +181,7 @@ void INA219Component::update() { if (this->power_sensor_ != nullptr) { uint16_t raw_power; - if (!this->read_byte_16(INA219_REGISTER_POWER, &raw_power, 1)) { + if (!this->read_byte_16(INA219_REGISTER_POWER, &raw_power)) { this->status_set_warning(); return; } diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index ed88ace967..020be9bc6e 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -13,7 +13,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,20 +31,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA219Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index 701a833041..2e30a5ac01 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -1,5 +1,6 @@ #include "ina226.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina226 { @@ -96,7 +97,7 @@ float INA226Component::get_setup_priority() const { return setup_priority::DATA; void INA226Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; - if (!this->read_byte_16(INA226_REGISTER_BUS_VOLTAGE, &raw_bus_voltage, 1)) { + if (!this->read_byte_16(INA226_REGISTER_BUS_VOLTAGE, &raw_bus_voltage)) { this->status_set_warning(); return; } @@ -106,8 +107,9 @@ void INA226Component::update() { if (this->shunt_voltage_sensor_ != nullptr) { uint16_t raw_shunt_voltage; - if (!this->read_byte_16(INA226_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage, 1)) { + if (!this->read_byte_16(INA226_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage)) { this->status_set_warning(); + return; } float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f; this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); @@ -115,7 +117,7 @@ void INA226Component::update() { if (this->current_sensor_ != nullptr) { uint16_t raw_current; - if (!this->read_byte_16(INA226_REGISTER_CURRENT, &raw_current, 1)) { + if (!this->read_byte_16(INA226_REGISTER_CURRENT, &raw_current)) { this->status_set_warning(); return; } @@ -125,7 +127,7 @@ void INA226Component::update() { if (this->power_sensor_ != nullptr) { uint16_t raw_power; - if (!this->read_byte_16(INA226_REGISTER_POWER, &raw_power, 1)) { + if (!this->read_byte_16(INA226_REGISTER_POWER, &raw_power)) { this->status_set_warning(); return; } diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index e4ceda39c1..ee4036ce7e 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -31,20 +30,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA226Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0) diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index f2fcdb21eb..3f8e2d06df 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -1,5 +1,6 @@ #include "ina3221.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ina3221 { @@ -87,7 +88,7 @@ void INA3221Component::update() { float bus_voltage_v = NAN, current_a = NAN; uint16_t raw; if (channel.should_measure_bus_voltage()) { - if (!this->read_byte_16(ina3221_bus_voltage_register(i), &raw, 1)) { + if (!this->read_byte_16(ina3221_bus_voltage_register(i), &raw)) { this->status_set_warning(); return; } @@ -96,7 +97,7 @@ void INA3221Component::update() { channel.bus_voltage_sensor_->publish_state(bus_voltage_v); } if (channel.should_measure_shunt_voltage()) { - if (!this->read_byte_16(ina3221_shunt_voltage_register(i), &raw, 1)) { + if (!this->read_byte_16(ina3221_shunt_voltage_register(i), &raw)) { this->status_set_warning(); return; } diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 8b861d972d..9c42ecbb9d 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,16 +31,28 @@ INA3221Component = ina3221_ns.class_( INA3221_CHANNEL_SCHEMA = cv.Schema( { cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + 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_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 03bc4f9f92..76013e28ff 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -1,30 +1,31 @@ #include "inkbird_ibsth1_mini.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace inkbird_ibsth1_mini { static const char *const TAG = "inkbird_ibsth1_mini"; -void InkbirdIBSTH1_MINI::dump_config() { +void InkbirdIbstH1Mini::dump_config() { ESP_LOGCONFIG(TAG, "Inkbird IBS TH1 MINI"); LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External Temperature", this->external_temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { +bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // The below is based on my research and reverse engineering of a single device // It is entirely possible that some of that may be inaccurate or incomplete // for Inkbird IBS-TH1 Mini device we expect // 1) expected mac address // 2) device address type == PUBLIC - // 3) no service datas - // 4) one manufacturer datas - // 5) the manufacturer datas should contain a 16-bit uuid amd a 7-byte data vector + // 3) no service data + // 4) one manufacturer data + // 5) the manufacturer data should contain a 16-bit uuid amd a 7-byte data vector // 6) the 7-byte data component should have data[2] == 0 and data[6] == 8 // the address should match the address we declared @@ -36,25 +37,25 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi ESP_LOGVV(TAG, "parse_device(): address is not public"); return false; } - if (device.get_service_datas().size() != 0) { + if (!device.get_service_datas().empty()) { ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty"); return false; } - auto mnfDatas = device.get_manufacturer_datas(); - if (mnfDatas.size() != 1) { + auto mnf_datas = device.get_manufacturer_datas(); + if (mnf_datas.size() != 1) { ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element"); return false; } - auto mnfData = mnfDatas[0]; - if (mnfData.uuid.get_uuid().len != ESP_UUID_LEN_16) { + auto mnf_data = mnf_datas[0]; + if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) { ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16"); return false; } - if (mnfData.data.size() != 7) { + if (mnf_data.data.size() != 7) { ESP_LOGVV(TAG, "parse_device(): manufacturer data element length is expected to be of length 7"); return false; } - if ((mnfData.data[2] != 0) || (mnfData.data[6] != 8)) { + if (mnf_data.data[6] != 8) { ESP_LOGVV(TAG, "parse_device(): unexpected data"); return false; } @@ -62,14 +63,37 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // sensor output encoding // data[5] is a battery level // data[0] and data[1] is humidity * 100 (in pct) - // uuid is a temperature * 100 (in Celcius) - auto battery_level = mnfData.data[5]; - auto temperature = mnfData.uuid.get_uuid().uuid.uuid16 / 100.0f; - auto humidity = ((mnfData.data[1] << 8) + mnfData.data[0]) / 100.0f; + // uuid is a temperature * 100 (in Celsius) + // when data[2] == 0 temperature is from internal sensor (IBS-TH1 or IBS-TH1 Mini) + // when data[2] == 1 temperature is from external sensor (IBS-TH1 only) - if (this->temperature_ != nullptr) { + // Create empty variables to pass automatic checks + auto temperature = NAN; + auto external_temperature = NAN; + + // Read bluetooth data into variable + auto measured_temperature = ((int16_t) mnf_data.uuid.get_uuid().uuid.uuid16) / 100.0f; + + // Set temperature or external_temperature based on which sensor is in use + if (mnf_data.data[2] == 0) { + temperature = measured_temperature; + } else if (mnf_data.data[2] == 1) { + external_temperature = measured_temperature; + } else { + ESP_LOGVV(TAG, "parse_device(): unknown sensor type"); + return false; + } + + auto battery_level = mnf_data.data[5]; + auto humidity = ((mnf_data.data[1] << 8) + mnf_data.data[0]) / 100.0f; + + // Send temperature only if the value is set + if (!std::isnan(temperature) && this->temperature_ != nullptr) { this->temperature_->publish_state(temperature); } + if (!std::isnan(external_temperature) && this->external_temperature_ != nullptr) { + this->external_temperature_->publish_state(external_temperature); + } if (this->humidity_ != nullptr) { this->humidity_->publish_state(humidity); } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index 38e72dad17..bdca2d0cac 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -4,12 +4,12 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace inkbird_ibsth1_mini { -class InkbirdIBSTH1_MINI : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -18,12 +18,14 @@ class InkbirdIBSTH1_MINI : public Component, public esp32_ble_tracker::ESPBTDevi void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } protected: uint64_t address_; sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; }; diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index 044e7fe67d..0ab9f8b3e0 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -19,36 +18,41 @@ 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") -InkbirdUBSTH1_MINI = inkbird_ibsth1_mini_ns.class_( - "InkbirdIBSTH1_MINI", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_( + "InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component ) CONFIG_SCHEMA = ( cv.Schema( { - cv.GenerateID(): cv.declare_id(InkbirdUBSTH1_MINI), + cv.GenerateID(): cv.declare_id(InkbirdIbstH1Mini), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -67,6 +71,9 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) + if CONF_EXTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_EXTERNAL_TEMPERATURE]) + cg.add(var.set_external_temperature(sens)) if CONF_HUMIDITY in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index 8e00a69751..e4c71ea717 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -8,11 +8,9 @@ from esphome.const import ( CONF_LAMBDA, CONF_PAGES, CONF_WAKEUP_PIN, - ESP_PLATFORM_ESP32, ) -DEPENDENCIES = ["i2c"] -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["i2c", "esp32"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" @@ -91,6 +89,7 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("5s")) .extend(i2c.i2c_device_schema(0x48)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + cv.only_with_arduino, ) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 089b4791a6..8a05836db9 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -3,7 +3,9 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include namespace esphome { namespace inkplate6 { @@ -150,7 +152,6 @@ void Inkplate6::dump_config() { } void Inkplate6::eink_off_() { ESP_LOGV(TAG, "Eink off called"); - unsigned long start_time = millis(); if (panel_on_ == 0) return; panel_on_ = 0; @@ -170,7 +171,6 @@ void Inkplate6::eink_off_() { } void Inkplate6::eink_on_() { ESP_LOGV(TAG, "Eink on called"); - unsigned long start_time = millis(); if (panel_on_ == 1) return; panel_on_ = 1; @@ -187,7 +187,7 @@ void Inkplate6::eink_on_() { delay(2); - this->read_byte(0x00, &temperature_, 0); + this->read_register(0x00, nullptr, 0); this->le_pin_->digital_write(false); this->oe_pin_->digital_write(false); @@ -200,7 +200,7 @@ void Inkplate6::eink_on_() { } void Inkplate6::fill(Color color) { ESP_LOGV(TAG, "Fill called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); if (this->greyscale_) { uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; @@ -210,26 +210,26 @@ void Inkplate6::fill(Color color) { memset(this->partial_buffer_, fill, this->get_buffer_length_()); } - ESP_LOGV(TAG, "Fill finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); } void Inkplate6::display() { ESP_LOGV(TAG, "Display called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); if (this->greyscale_) { this->display3b_(); } else { if (this->partial_updating_ && this->partial_update_()) { - ESP_LOGV(TAG, "Display finished (partial) (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (partial) (%ums)", millis() - start_time); return; } this->display1b_(); } - ESP_LOGV(TAG, "Display finished (full) (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); } void Inkplate6::display1b_() { ESP_LOGV(TAG, "Display1b called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); @@ -248,32 +248,32 @@ void Inkplate6::display1b_() { clean_fast_(0, 11); uint32_t clock = (1 << this->cl_pin_->get_pin()); - ESP_LOGV(TAG, "Display1b start loops (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); for (int k = 0; k < 3; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); for (int i = 0; i < this->get_height_internal(); i++) { buffer_value = *(buffer_ptr--); data = LUTB[(buffer_value >> 4) & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25); + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); hscan_start_(send); data = LUTB[buffer_value & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { buffer_value = *(buffer_ptr--); data = LUTB[(buffer_value >> 4) & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; data = LUTB[buffer_value & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; } @@ -283,31 +283,31 @@ void Inkplate6::display1b_() { } delayMicroseconds(230); } - ESP_LOGV(TAG, "Display1b first loop x %d (%lums)", 3, millis() - start_time); + ESP_LOGV(TAG, "Display1b first loop x %d (%ums)", 3, millis() - start_time); buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); for (int i = 0; i < this->get_height_internal(); i++) { buffer_value = *(buffer_ptr--); data = LUT2[(buffer_value >> 4) & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25); + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); hscan_start_(send); data = LUT2[buffer_value & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { buffer_value = *(buffer_ptr--); data = LUT2[(buffer_value >> 4) & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; data = LUT2[buffer_value & 0x0F]; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; } @@ -316,13 +316,13 @@ void Inkplate6::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b second loop (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b second loop (%ums)", millis() - start_time); vscan_start_(); for (int i = 0; i < this->get_height_internal(); i++) { data = 0b00000000; - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25); + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); hscan_start_(send); send |= clock; GPIO.out_w1ts = send; @@ -338,17 +338,17 @@ void Inkplate6::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b third loop (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b third loop (%ums)", millis() - start_time); vscan_start_(); eink_off_(); this->block_partial_ = false; this->partial_updates_ = 0; - ESP_LOGV(TAG, "Display1b finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); } void Inkplate6::display3b_() { ESP_LOGV(TAG, "Display3b called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); eink_on_(); clean_fast_(0, 1); @@ -382,11 +382,11 @@ void Inkplate6::display3b_() { pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); - send = ((pixel & B00000011) << 4) | (((pixel & B00001100) >> 2) << 18) | (((pixel & B00010000) >> 4) << 23) | - (((pixel & B11100000) >> 5) << 25); + send = ((pixel & 0b00000011) << 4) | (((pixel & 0b00001100) >> 2) << 18) | (((pixel & 0b00010000) >> 4) << 23) | + (((pixel & 0b11100000) >> 5) << 25); hscan_start_(send); - send = ((pixel2 & B00000011) << 4) | (((pixel2 & B00001100) >> 2) << 18) | (((pixel2 & B00010000) >> 4) << 23) | - (((pixel2 & B11100000) >> 5) << 25) | clock; + send = ((pixel2 & 0b00000011) << 4) | (((pixel2 & 0b00001100) >> 2) << 18) | + (((pixel2 & 0b00010000) >> 4) << 23) | (((pixel2 & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; @@ -400,13 +400,13 @@ void Inkplate6::display3b_() { pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); - send = ((pixel & B00000011) << 4) | (((pixel & B00001100) >> 2) << 18) | (((pixel & B00010000) >> 4) << 23) | - (((pixel & B11100000) >> 5) << 25) | clock; + send = ((pixel & 0b00000011) << 4) | (((pixel & 0b00001100) >> 2) << 18) | (((pixel & 0b00010000) >> 4) << 23) | + (((pixel & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; - send = ((pixel2 & B00000011) << 4) | (((pixel2 & B00001100) >> 2) << 18) | (((pixel2 & B00010000) >> 4) << 23) | - (((pixel2 & B11100000) >> 5) << 25) | clock; + send = ((pixel2 & 0b00000011) << 4) | (((pixel2 & 0b00001100) >> 2) << 18) | + (((pixel2 & 0b00010000) >> 4) << 23) | (((pixel2 & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; } @@ -420,11 +420,11 @@ void Inkplate6::display3b_() { clean_fast_(3, 1); vscan_start_(); eink_off_(); - ESP_LOGV(TAG, "Display3b finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); } bool Inkplate6::partial_update_() { ESP_LOGV(TAG, "Partial update called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); if (this->greyscale_) return false; if (this->block_partial_) @@ -447,7 +447,7 @@ bool Inkplate6::partial_update_() { this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; } } - ESP_LOGV(TAG, "Partial update buffer built after (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update buffer built after (%ums)", millis() - start_time); eink_on_(); uint32_t clock = (1 << this->cl_pin_->get_pin()); @@ -456,13 +456,13 @@ bool Inkplate6::partial_update_() { 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++) { data = *(data_ptr--); - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25); + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); hscan_start_(send); for (int j = 0, jm = (this->get_width_internal() / 4) - 1; j < jm; j++) { data = *(data_ptr--); - send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25) | clock; + send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25) | clock; GPIO.out_w1ts = send; GPIO.out_w1tc = send; } @@ -471,7 +471,7 @@ bool Inkplate6::partial_update_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Partial update loop k=%d (%lums)", k, millis() - start_time); + ESP_LOGV(TAG, "Partial update loop k=%d (%ums)", k, millis() - start_time); } clean_fast_(2, 2); clean_fast_(3, 1); @@ -479,7 +479,7 @@ bool Inkplate6::partial_update_() { eink_off_(); memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); - ESP_LOGV(TAG, "Partial update finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update finished (%ums)", millis() - start_time); return true; } void Inkplate6::vscan_start_() { @@ -531,7 +531,7 @@ void Inkplate6::vscan_end_() { } void Inkplate6::clean() { ESP_LOGV(TAG, "Clean called"); - unsigned long start_time = millis(); + uint32_t start_time = millis(); eink_on_(); clean_fast_(0, 1); // White @@ -540,25 +540,25 @@ void Inkplate6::clean() { clean_fast_(0, 8); // Black to Black clean_fast_(2, 1); // Black to White clean_fast_(1, 10); // White to White - ESP_LOGV(TAG, "Clean finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); } void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); - unsigned long start_time = millis(); + uint32_t start_time = millis(); eink_on_(); uint8_t data = 0; if (c == 0) // White - data = B10101010; + data = 0b10101010; else if (c == 1) // Black - data = B01010101; + data = 0b01010101; else if (c == 2) // Discharge - data = B00000000; + data = 0b00000000; else if (c == 3) // Skip - data = B11111111; + data = 0b11111111; - uint32_t send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | - (((data & B11100000) >> 5) << 25); + uint32_t send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | + (((data & 0b11100000) >> 5) << 25); uint32_t clock = (1 << this->cl_pin_->get_pin()); for (int k = 0; k < rep; k++) { @@ -578,46 +578,46 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Clean fast rep loop %d finished (%lums)", k, millis() - start_time); + ESP_LOGV(TAG, "Clean fast rep loop %d finished (%ums)", k, millis() - start_time); } - ESP_LOGV(TAG, "Clean fast finished (%lums)", millis() - start_time); + ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); } void Inkplate6::pins_z_state_() { - this->ckv_pin_->pin_mode(INPUT); - this->sph_pin_->pin_mode(INPUT); + this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); + this->sph_pin_->pin_mode(gpio::FLAG_INPUT); - this->oe_pin_->pin_mode(INPUT); - this->gmod_pin_->pin_mode(INPUT); - this->spv_pin_->pin_mode(INPUT); + this->oe_pin_->pin_mode(gpio::FLAG_INPUT); + this->gmod_pin_->pin_mode(gpio::FLAG_INPUT); + this->spv_pin_->pin_mode(gpio::FLAG_INPUT); - this->display_data_0_pin_->pin_mode(INPUT); - this->display_data_1_pin_->pin_mode(INPUT); - this->display_data_2_pin_->pin_mode(INPUT); - this->display_data_3_pin_->pin_mode(INPUT); - this->display_data_4_pin_->pin_mode(INPUT); - this->display_data_5_pin_->pin_mode(INPUT); - this->display_data_6_pin_->pin_mode(INPUT); - this->display_data_7_pin_->pin_mode(INPUT); + this->display_data_0_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_1_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_2_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_3_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_4_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_5_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_6_pin_->pin_mode(gpio::FLAG_INPUT); + this->display_data_7_pin_->pin_mode(gpio::FLAG_INPUT); } void Inkplate6::pins_as_outputs_() { - this->ckv_pin_->pin_mode(OUTPUT); - this->sph_pin_->pin_mode(OUTPUT); + this->ckv_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->sph_pin_->pin_mode(gpio::FLAG_OUTPUT); - this->oe_pin_->pin_mode(OUTPUT); - this->gmod_pin_->pin_mode(OUTPUT); - this->spv_pin_->pin_mode(OUTPUT); + this->oe_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->gmod_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->spv_pin_->pin_mode(gpio::FLAG_OUTPUT); - this->display_data_0_pin_->pin_mode(OUTPUT); - this->display_data_1_pin_->pin_mode(OUTPUT); - this->display_data_2_pin_->pin_mode(OUTPUT); - this->display_data_3_pin_->pin_mode(OUTPUT); - this->display_data_4_pin_->pin_mode(OUTPUT); - this->display_data_5_pin_->pin_mode(OUTPUT); - this->display_data_6_pin_->pin_mode(OUTPUT); - this->display_data_7_pin_->pin_mode(OUTPUT); + this->display_data_0_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_1_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_2_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_3_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_4_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_5_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_6_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->display_data_7_pin_->pin_mode(gpio::FLAG_OUTPUT); } } // namespace inkplate6 } // namespace esphome -#endif +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 94d14b4f6e..56e95e95bb 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -1,25 +1,29 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" #include "esphome/components/display/display_buffer.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO namespace esphome { namespace inkplate6 { class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { public: - const uint8_t LUT2[16] = {B10101010, B10101001, B10100110, B10100101, B10011010, B10011001, B10010110, B10010101, - B01101010, B01101001, B01100110, B01100101, B01011010, B01011001, B01010110, B01010101}; - const uint8_t LUTW[16] = {B11111111, B11111110, B11111011, B11111010, B11101111, B11101110, B11101011, B11101010, - B10111111, B10111110, B10111011, B10111010, B10101111, B10101110, B10101011, B10101010}; - const uint8_t LUTB[16] = {B11111111, B11111101, B11110111, B11110101, B11011111, B11011101, B11010111, B11010101, - B01111111, B01111101, B01110111, B01110101, B01011111, B01011101, B01010111, B01010101}; - const uint8_t pixelMaskLUT[8] = {B00000001, B00000010, B00000100, B00001000, - B00010000, B00100000, B01000000, B10000000}; - const uint8_t pixelMaskGLUT[2] = {B00001111, B11110000}; + const uint8_t LUT2[16] = {0b10101010, 0b10101001, 0b10100110, 0b10100101, 0b10011010, 0b10011001, + 0b10010110, 0b10010101, 0b01101010, 0b01101001, 0b01100110, 0b01100101, + 0b01011010, 0b01011001, 0b01010110, 0b01010101}; + const uint8_t LUTW[16] = {0b11111111, 0b11111110, 0b11111011, 0b11111010, 0b11101111, 0b11101110, + 0b11101011, 0b11101010, 0b10111111, 0b10111110, 0b10111011, 0b10111010, + 0b10101111, 0b10101110, 0b10101011, 0b10101010}; + const uint8_t LUTB[16] = {0b11111111, 0b11111101, 0b11110111, 0b11110101, 0b11011111, 0b11011101, + 0b11010111, 0b11010101, 0b01111111, 0b01111101, 0b01110111, 0b01110101, + 0b01011111, 0b01011101, 0b01010111, 0b01010101}; + const uint8_t pixelMaskLUT[8] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, + 0b00010000, 0b00100000, 0b01000000, 0b10000000}; + const uint8_t pixelMaskGLUT[2] = {0b00001111, 0b11110000}; const uint8_t waveform3Bit[8][8] = {{0, 0, 0, 0, 1, 1, 1, 0}, {1, 2, 2, 2, 1, 1, 1, 0}, {0, 1, 2, 1, 1, 2, 1, 0}, {0, 2, 1, 2, 1, 2, 1, 0}, {0, 0, 0, 1, 1, 1, 2, 0}, {2, 1, 1, 1, 2, 1, 2, 0}, {1, 1, 1, 2, 1, 2, 2, 0}, {0, 0, 0, 0, 0, 0, 2, 0}}; @@ -39,20 +43,20 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public 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; } - void set_display_data_0_pin(GPIOPin *data) { this->display_data_0_pin_ = data; } - void set_display_data_1_pin(GPIOPin *data) { this->display_data_1_pin_ = data; } - void set_display_data_2_pin(GPIOPin *data) { this->display_data_2_pin_ = data; } - void set_display_data_3_pin(GPIOPin *data) { this->display_data_3_pin_ = data; } - void set_display_data_4_pin(GPIOPin *data) { this->display_data_4_pin_ = data; } - void set_display_data_5_pin(GPIOPin *data) { this->display_data_5_pin_ = data; } - void set_display_data_6_pin(GPIOPin *data) { this->display_data_6_pin_ = data; } - void set_display_data_7_pin(GPIOPin *data) { this->display_data_7_pin_ = data; } + void set_display_data_0_pin(InternalGPIOPin *data) { this->display_data_0_pin_ = data; } + void set_display_data_1_pin(InternalGPIOPin *data) { this->display_data_1_pin_ = data; } + void set_display_data_2_pin(InternalGPIOPin *data) { this->display_data_2_pin_ = data; } + void set_display_data_3_pin(InternalGPIOPin *data) { this->display_data_3_pin_ = data; } + void set_display_data_4_pin(InternalGPIOPin *data) { this->display_data_4_pin_ = data; } + void set_display_data_5_pin(InternalGPIOPin *data) { this->display_data_5_pin_ = data; } + void set_display_data_6_pin(InternalGPIOPin *data) { this->display_data_6_pin_ = data; } + void set_display_data_7_pin(InternalGPIOPin *data) { this->display_data_7_pin_ = data; } void set_ckv_pin(GPIOPin *ckv) { this->ckv_pin_ = ckv; } - void set_cl_pin(GPIOPin *cl) { this->cl_pin_ = cl; } + void set_cl_pin(InternalGPIOPin *cl) { this->cl_pin_ = cl; } void set_gpio0_enable_pin(GPIOPin *gpio0_enable) { this->gpio0_enable_pin_ = gpio0_enable; } void set_gmod_pin(GPIOPin *gmod) { this->gmod_pin_ = gmod; } - void set_le_pin(GPIOPin *le) { this->le_pin_ = le; } + void set_le_pin(InternalGPIOPin *le) { this->le_pin_ = le; } void set_oe_pin(GPIOPin *oe) { this->oe_pin_ = oe; } void set_powerup_pin(GPIOPin *powerup) { this->powerup_pin_ = powerup; } void set_sph_pin(GPIOPin *sph) { this->sph_pin_ = sph; } @@ -129,20 +133,20 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public bool greyscale_; bool partial_updating_; - GPIOPin *display_data_0_pin_; - GPIOPin *display_data_1_pin_; - GPIOPin *display_data_2_pin_; - GPIOPin *display_data_3_pin_; - GPIOPin *display_data_4_pin_; - GPIOPin *display_data_5_pin_; - GPIOPin *display_data_6_pin_; - GPIOPin *display_data_7_pin_; + InternalGPIOPin *display_data_0_pin_; + InternalGPIOPin *display_data_1_pin_; + InternalGPIOPin *display_data_2_pin_; + InternalGPIOPin *display_data_3_pin_; + InternalGPIOPin *display_data_4_pin_; + InternalGPIOPin *display_data_5_pin_; + InternalGPIOPin *display_data_6_pin_; + InternalGPIOPin *display_data_7_pin_; GPIOPin *ckv_pin_; - GPIOPin *cl_pin_; + InternalGPIOPin *cl_pin_; GPIOPin *gpio0_enable_pin_; GPIOPin *gmod_pin_; - GPIOPin *le_pin_; + InternalGPIOPin *le_pin_; GPIOPin *oe_pin_; GPIOPin *powerup_pin_; GPIOPin *sph_pin_; @@ -154,4 +158,4 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public } // namespace inkplate6 } // namespace esphome -#endif +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index bfcf8d3561..2a398e5240 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -1,6 +1,7 @@ #include "integration_sensor.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace integration { @@ -9,13 +10,15 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); float preference_value = 0; this->rtc_.load(&preference_value); this->result_ = preference_value; } this->last_update_ = millis(); + this->last_save_ = this->last_update_; + this->publish_and_save_(this->result_); this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); } diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 2fcec069b2..437649c1dd 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/automation.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -27,6 +28,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } + void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } @@ -55,10 +57,13 @@ class IntegrationSensor : public sensor::Sensor, public Component { this->result_ = result; this->publish_state(result); float result_f = result; + const uint32_t now = millis(); + if (now - this->last_save_ < this->min_save_interval_) + return; + this->last_save_ = now; this->rtc_.save(&result_f); } std::string unit_of_measurement() override; - std::string icon() override { return this->sensor_->get_icon(); } int8_t accuracy_decimals() override { return this->sensor_->get_accuracy_decimals() + 2; } sensor::Sensor *sensor_; @@ -67,6 +72,8 @@ class IntegrationSensor : public sensor::Sensor, public Component { bool restore_; ESPPreferenceObject rtc_; + uint32_t last_save_{0}; + uint32_t min_save_interval_{0}; uint32_t last_update_; double result_{0.0f}; float last_value_{0.0f}; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 3f32394ff6..26c7c2871a 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import sensor -from esphome.const import CONF_ID, CONF_SENSOR, CONF_RESTORE +from esphome.const import CONF_ICON, CONF_ID, CONF_SENSOR, CONF_RESTORE +from esphome.core.entity_helpers import inherit_property_from integration_ns = cg.esphome_ns.namespace("integration") IntegrationSensor = integration_ns.class_( @@ -27,6 +28,7 @@ INTEGRATION_METHODS = { CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { @@ -37,10 +39,26 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( INTEGRATION_METHODS, lower=True ), cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional( + CONF_MIN_SAVE_INTERVAL, default="0s" + ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(IntegrationSensor), + cv.Optional(CONF_ICON): cv.icon, + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + }, + extra=cv.ALLOW_EXTRA, + ), + inherit_property_from(CONF_ICON, CONF_SENSOR), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -52,6 +70,7 @@ async def to_code(config): cg.add(var.set_time(config[CONF_TIME_UNIT])) cg.add(var.set_method(config[CONF_INTEGRATION_METHOD])) cg.add(var.set_restore(config[CONF_RESTORE])) + cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) @automation.register_action( diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 7bdef6ea0e..fda0a552f1 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,12 +1,18 @@ import esphome.codegen as cg +import esphome.config_validation as cv from esphome.core import coroutine_with_priority CODEOWNERS = ["@OttoWinter"] json_ns = cg.esphome_ns.namespace("json") +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_arduino, +) + @coroutine_with_priority(1.0) async def to_code(config): - cg.add_library("ArduinoJson-esphomelib", "5.13.3") + cg.add_library("ottowinter/ArduinoJson-esphomelib", "5.13.3") cg.add_define("USE_JSON") cg.add_global(json_ns.using) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 4720c8f9c9..12c5beb73f 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "json_util.h" #include "esphome/core/log.h" @@ -6,21 +8,7 @@ namespace json { static const char *const TAG = "json"; -static char *global_json_build_buffer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static size_t global_json_build_buffer_size = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void reserve_global_json_build_buffer(size_t required_size) { - if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) { - delete[] global_json_build_buffer; - global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2); - - size_t remainder = global_json_build_buffer_size % 16U; - if (remainder != 0) - global_json_build_buffer_size += 16 - remainder; - - global_json_build_buffer = new char[global_json_build_buffer_size]; - } -} +static std::vector global_json_build_buffer; // NOLINT const char *build_json(const json_build_t &f, size_t *length) { global_json_buffer.clear(); @@ -35,16 +23,16 @@ const char *build_json(const json_build_t &f, size_t *length) { // Discovery | 372 | 356 | // Discovery | 336 | 311 | // Discovery | 408 | 393 | - reserve_global_json_build_buffer(global_json_buffer.size()); - size_t bytes_written = root.printTo(global_json_build_buffer, global_json_build_buffer_size); + global_json_build_buffer.reserve(global_json_buffer.size() + 1); + size_t bytes_written = root.printTo(global_json_build_buffer.data(), global_json_build_buffer.capacity()); - if (bytes_written >= global_json_build_buffer_size - 1) { - reserve_global_json_build_buffer(root.measureLength() + 1); - bytes_written = root.printTo(global_json_build_buffer, global_json_build_buffer_size); + if (bytes_written >= global_json_build_buffer.capacity() - 1) { + global_json_build_buffer.reserve(root.measureLength() + 1); + bytes_written = root.printTo(global_json_build_buffer.data(), global_json_build_buffer.capacity()); } *length = bytes_written; - return global_json_build_buffer; + return global_json_build_buffer.data(); } void parse_json(const std::string &data, const json_parse_t &f) { global_json_buffer.clear(); @@ -113,7 +101,7 @@ void VectorJsonBuffer::reserve(size_t size) { // NOLINT target_capacity *= 2; char *old_buffer = this->buffer_; - this->buffer_ = new char[target_capacity]; + this->buffer_ = new char[target_capacity]; // NOLINT if (old_buffer != nullptr && this->capacity_ != 0) { this->free_blocks_.push_back(old_buffer); memcpy(this->buffer_, old_buffer, this->capacity_); @@ -127,3 +115,5 @@ VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-cons } // namespace json } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index aafd039b9a..577510e63a 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,5 +1,9 @@ #pragma once +#ifdef USE_ARDUINO + +#include + #include "esphome/core/helpers.h" #include @@ -60,3 +64,5 @@ extern VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-n } // namespace json } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 95b349b9c1..ddd7d6a6b3 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -1,6 +1,7 @@ #include "lcd_display.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace lcd_base { @@ -29,7 +30,7 @@ static const uint8_t LCD_DISPLAY_FUNCTION_2_LINE = 0x08; static const uint8_t LCD_DISPLAY_FUNCTION_5X10_DOTS = 0x04; void LCDDisplay::setup() { - this->buffer_ = new uint8_t[this->rows_ * this->columns_]; + this->buffer_ = new uint8_t[this->rows_ * this->columns_]; // NOLINT for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index 6b0a2f69de..9fb635eafa 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -20,8 +20,7 @@ GPIOLCDDisplay = lcd_gpio_ns.class_("GPIOLCDDisplay", lcd_base.LCDDisplay) def validate_pin_length(value): if len(value) != 4 and len(value) != 8: raise cv.Invalid( - "LCD Displays can either operate in 4-pin or 8-pin mode," - "not {}-pin mode".format(len(value)) + f"LCD Displays can either operate in 4-pin or 8-pin mode,not {len(value)}-pin mode" ) return value diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.cpp b/esphome/components/lcd_gpio/gpio_lcd_display.cpp index 5c1656ec3e..b0344d313c 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.cpp +++ b/esphome/components/lcd_gpio/gpio_lcd_display.cpp @@ -30,8 +30,15 @@ void GPIOLCDDisplay::dump_config() { LOG_PIN(" RW Pin: ", this->rw_pin_); LOG_PIN(" Enable Pin: ", this->enable_pin_); - for (uint8_t i = 0; i < (this->is_four_bit_mode() ? 4 : 8); i++) { - ESP_LOGCONFIG(TAG, " Data Pin %u" LOG_PIN_PATTERN, i, LOG_PIN_ARGS(this->data_pins_[i])); + LOG_PIN(" Data Pin 0: ", data_pins_[0]); + LOG_PIN(" Data Pin 1: ", data_pins_[1]); + LOG_PIN(" Data Pin 2: ", data_pins_[2]); + LOG_PIN(" Data Pin 3: ", data_pins_[3]); + if (!is_four_bit_mode()) { + LOG_PIN(" Data Pin 4: ", data_pins_[4]); + LOG_PIN(" Data Pin 5: ", data_pins_[5]); + LOG_PIN(" Data Pin 6: ", data_pins_[6]); + LOG_PIN(" Data Pin 7: ", data_pins_[7]); } LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index 01f6f95d9a..aba254a90a 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/lcd_base/lcd_display.h" namespace esphome { diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index 4830b6f223..5b00b08aff 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -1,5 +1,6 @@ #include "pcf8574_display.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace lcd_pcf8574 { diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 0575dbee6a..21a747e34d 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -1,40 +1,20 @@ #include "ledc_output.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 +#ifdef USE_ARDUINO #include +#endif +#ifdef USE_ESP_IDF +#include +#endif namespace esphome { namespace ledc { static const char *const TAG = "ledc.output"; -void LEDCOutput::write_state(float state) { - 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); - ledcWrite(this->channel_, duty); -} - -void LEDCOutput::setup() { - this->update_frequency(this->frequency_); - this->turn_off(); - // Attach pin after setting default value - ledcAttachPin(this->pin_->get_pin(), this->channel_); -} - -void LEDCOutput::dump_config() { - ESP_LOGCONFIG(TAG, "LEDC Output:"); - LOG_PIN(" Pin ", this->pin_); - ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); -} - float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) { const float max_div_num = ((1 << 20) - 1) / 256.0f; @@ -50,6 +30,73 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } +void LEDCOutput::write_state(float state) { + if (!initialized_) { + ESP_LOGW(TAG, "LEDC 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); + +#ifdef USE_ARDUINO + ledcWrite(this->channel_, duty); +#endif +#ifdef USE_ESP_IDF + auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + auto chan_num = static_cast(channel_ % 8); + ledc_set_duty(speed_mode, chan_num, duty); + ledc_update_duty(speed_mode, chan_num); +#endif +} + +void LEDCOutput::setup() { +#ifdef USE_ARDUINO + this->update_frequency(this->frequency_); + this->turn_off(); + // Attach pin after setting default value + ledcAttachPin(this->pin_->get_pin(), this->channel_); +#endif +#ifdef USE_ESP_IDF + auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + auto timer_num = static_cast((channel_ % 8) / 2); + auto chan_num = static_cast(channel_ % 8); + + bit_depth_ = *ledc_bit_depth_for_frequency(frequency_); + + ledc_timer_config_t timer_conf{}; + timer_conf.speed_mode = speed_mode; + timer_conf.duty_resolution = static_cast(bit_depth_); + timer_conf.timer_num = timer_num; + timer_conf.freq_hz = (uint32_t) frequency_; + timer_conf.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&timer_conf); + + ledc_channel_config_t chan_conf{}; + chan_conf.gpio_num = pin_->get_pin(); + chan_conf.speed_mode = speed_mode; + chan_conf.channel = chan_num; + 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; + ledc_channel_config(&chan_conf); + initialized_ = true; +#endif +} + +void LEDCOutput::dump_config() { + ESP_LOGCONFIG(TAG, "LEDC Output:"); + LOG_PIN(" Pin ", this->pin_); + ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); + ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); +} + void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { @@ -58,12 +105,31 @@ void LEDCOutput::update_frequency(float frequency) { } this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; +#ifdef USE_ARDUINO ledcSetup(this->channel_, frequency, this->bit_depth_); + initialized_ = true; +#endif // USE_ARDUINO +#ifdef USE_ESP_IDF + if (!initialized_) { + ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); + return; + } + auto speed_mode = channel_ < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; + auto timer_num = static_cast((channel_ % 8) / 2); + + ledc_timer_config_t timer_conf{}; + timer_conf.speed_mode = speed_mode; + timer_conf.duty_resolution = static_cast(bit_depth_); + timer_conf.timer_num = timer_num; + timer_conf.freq_hz = (uint32_t) frequency_; + timer_conf.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&timer_conf); +#endif // re-apply duty this->write_state(this->duty_); } -uint8_t next_ledc_channel = 0; +uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace ledc } // namespace esphome diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index b3b14fe855..a78bf440a9 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -1,20 +1,21 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ledc { +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern uint8_t next_ledc_channel; class LEDCOutput : public output::FloatOutput, public Component { public: - explicit LEDCOutput(GPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; } + explicit LEDCOutput(InternalGPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; } void set_channel(uint8_t channel) { this->channel_ = channel; } void set_frequency(float frequency) { this->frequency_ = frequency; } @@ -31,11 +32,12 @@ class LEDCOutput : public output::FloatOutput, public Component { void write_state(float state) override; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; float frequency_{}; float duty_{0.0f}; + bool initialized_ = false; }; template class SetFrequencyAction : public Action { diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 150c5fa410..895dcc998b 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -3,15 +3,13 @@ from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( - CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_PIN, - ESP_PLATFORM_ESP32, ) -ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +DEPENDENCIES = ["esp32"] def calc_max_frequency(bit_depth): @@ -29,13 +27,11 @@ def validate_frequency(value): max_freq = calc_max_frequency(1) if value < min_freq: raise cv.Invalid( - "This frequency setting is not possible, please choose a higher " - "frequency (at least {}Hz)".format(int(min_freq)) + f"This frequency setting is not possible, please choose a higher frequency (at least {int(min_freq)}Hz)" ) if value > max_freq: raise cv.Invalid( - "This frequency setting is not possible, please choose a lower " - "frequency (at most {}Hz)".format(int(max_freq)) + f"This frequency setting is not possible, please choose a lower frequency (at most {int(max_freq)}Hz)" ) return value @@ -50,10 +46,6 @@ 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_BIT_DEPTH): cv.invalid( - "The bit_depth option has been removed in v1.14, the " - "best bit depth is now automatically calculated." - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 276bf8073d..03224d4c10 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -6,18 +6,20 @@ from esphome.const import ( CONF_COLOR_CORRECT, CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, + CONF_FLASH_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, CONF_ID, - CONF_INTERNAL, - CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY, CONF_RESTORE_MODE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, ) from esphome.core import coroutine_with_priority +from esphome.cpp_helpers import setup_entity from .automation import light_control_to_code # noqa from .effects import ( validate_effects, @@ -50,7 +52,7 @@ RESTORE_MODES = { "RESTORE_INVERTED_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_ON, } -LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +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), @@ -82,6 +84,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( cv.Optional( CONF_DEFAULT_TRANSITION_LENGTH, default="1s" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_FLASH_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), } ) @@ -104,16 +109,35 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( ) +def validate_color_temperature_channels(value): + if ( + CONF_COLD_WHITE_COLOR_TEMPERATURE in value + and CONF_WARM_WHITE_COLOR_TEMPERATURE in value + and value[CONF_COLD_WHITE_COLOR_TEMPERATURE] + >= value[CONF_WARM_WHITE_COLOR_TEMPERATURE] + ): + raise cv.Invalid( + "Color temperature of the cold white channel must be colder than that of the warm white channel.", + path=[CONF_COLD_WHITE_COLOR_TEMPERATURE], + ) + return value + + async def setup_light_core_(light_var, output_var, config): + await setup_entity(light_var, config) + cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) - if CONF_INTERNAL in config: - cg.add(light_var.set_internal(config[CONF_INTERNAL])) + if CONF_DEFAULT_TRANSITION_LENGTH in config: cg.add( light_var.set_default_transition_length( config[CONF_DEFAULT_TRANSITION_LENGTH] ) ) + 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])) effects = await cg.build_registry_list( @@ -141,7 +165,7 @@ async def setup_light_core_(light_var, output_var, config): async def register_light(output_var, config): - light_var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME], output_var) + light_var = cg.new_Pvariable(config[CONF_ID], output_var) cg.add(cg.App.register_light(light_var)) await cg.register_component(light_var, config) await setup_light_core_(light_var, output_var, config) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index ecc48e32b8..f3e6c0ef1d 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -6,270 +6,107 @@ namespace light { static const char *const TAG = "light.addressable"; -Color ESPHSVColor::to_rgb() const { - // based on FastLED's hsv rainbow to rgb - const uint8_t hue = this->hue; - const uint8_t sat = this->saturation; - const uint8_t val = this->value; - // upper 3 hue bits are for branch selection, lower 5 are for values - const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 - // third of the offset, 255/3 = 85 (actually only up to 82; 164) - const uint8_t third = esp_scale8(offset8, 85); - const uint8_t two_thirds = esp_scale8(offset8, 170); - Color rgb(255, 255, 255, 0); - switch (hue >> 5) { - case 0b000: - rgb.r = 255 - third; - rgb.g = third; - rgb.b = 0; - break; - case 0b001: - rgb.r = 171; - rgb.g = 85 + third; - rgb.b = 0; - break; - case 0b010: - rgb.r = 171 - two_thirds; - rgb.g = 170 + third; - rgb.b = 0; - break; - case 0b011: - rgb.r = 0; - rgb.g = 255 - third; - rgb.b = third; - break; - case 0b100: - rgb.r = 0; - rgb.g = 171 - two_thirds; - rgb.b = 85 + two_thirds; - break; - case 0b101: - rgb.r = third; - rgb.g = 0; - rgb.b = 255 - third; - break; - case 0b110: - rgb.r = 85 + third; - rgb.g = 0; - rgb.b = 171 - third; - break; - case 0b111: - rgb.r = 170 + third; - rgb.g = 0; - rgb.b = 85 - third; - break; - default: - break; - } - // low saturation -> add uniform color to orig. hue - // high saturation -> use hue directly - // scales with square of saturation - // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 - rgb *= sat; - const uint8_t desat = 255 - sat; - rgb += esp_scale8(desat, desat); - // (r,g,b) = (r,g,b) * val - rgb *= val; - return rgb; -} - -void ESPRangeView::set(const Color &color) { - for (int32_t i = this->begin_; i < this->end_; i++) { - (*this->parent_)[i] = color; - } -} -ESPColorView ESPRangeView::operator[](int32_t index) const { - index = interpret_index(index, this->size()) + this->begin_; - return (*this->parent_)[index]; -} -ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } -ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } -void ESPRangeView::set_red(uint8_t red) { - for (auto c : *this) - c.set_red(red); -} -void ESPRangeView::set_green(uint8_t green) { - for (auto c : *this) - c.set_green(green); -} -void ESPRangeView::set_blue(uint8_t blue) { - for (auto c : *this) - c.set_blue(blue); -} -void ESPRangeView::set_white(uint8_t white) { - for (auto c : *this) - c.set_white(white); -} -void ESPRangeView::set_effect_data(uint8_t effect_data) { - for (auto c : *this) - c.set_effect_data(effect_data); -} -void ESPRangeView::fade_to_white(uint8_t amnt) { - for (auto c : *this) - c.fade_to_white(amnt); -} -void ESPRangeView::fade_to_black(uint8_t amnt) { - for (auto c : *this) - c.fade_to_black(amnt); -} -void ESPRangeView::lighten(uint8_t delta) { - for (auto c : *this) - c.lighten(delta); -} -void ESPRangeView::darken(uint8_t delta) { - for (auto c : *this) - c.darken(delta); -} -ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT - // If size doesn't match, error (todo warning) - if (rhs.size() != this->size()) - return *this; - - if (this->parent_ != rhs.parent_) { - for (int32_t i = 0; i < this->size(); i++) - (*this)[i].set(rhs[i].get()); - return *this; - } - - // If both equal, already done - if (rhs.begin_ == this->begin_) - return *this; - - if (rhs.begin_ > this->begin_) { - // Copy from left - for (int32_t i = 0; i < this->size(); i++) { - (*this)[i].set(rhs[i].get()); - } - } else { - // Copy from right - for (int32_t i = this->size() - 1; i >= 0; i--) { - (*this)[i].set(rhs[i].get()); - } - } - - return *this; -} - -ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } - -int32_t HOT interpret_index(int32_t index, int32_t size) { - if (index < 0) - return size + index; - return index; -} - void AddressableLight::call_setup() { this->setup(); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE this->set_interval(5000, [this]() { const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); - ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_), - YESNO(this->next_show_)); + ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_)); for (int i = 0; i < this->size(); i++) { auto color = this->get(i); ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), color.get_blue_raw(), color.get_white_raw()); } - ESP_LOGVV(TAG, ""); + ESP_LOGVV(TAG, " "); }); #endif } -Color esp_color_from_light_color_values(LightColorValues val) { - auto r = static_cast(roundf(val.get_red() * 255.0f)); - auto g = static_cast(roundf(val.get_green() * 255.0f)); - auto b = static_cast(roundf(val.get_blue() * 255.0f)); - auto w = static_cast(roundf(val.get_white() * val.get_state() * 255.0f)); +std::unique_ptr AddressableLight::create_default_transition() { + return make_unique(*this); +} + +Color color_from_light_color_values(LightColorValues val) { + auto r = to_uint8_scale(val.get_color_brightness() * val.get_red()); + auto g = to_uint8_scale(val.get_color_brightness() * val.get_green()); + auto b = to_uint8_scale(val.get_color_brightness() * val.get_blue()); + auto w = to_uint8_scale(val.get_white()); return Color(r, g, b, w); } -void AddressableLight::write_state(LightState *state) { +void AddressableLight::update_state(LightState *state) { auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); + auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); this->correction_.set_local_brightness(max_brightness); - this->last_transition_progress_ = 0.0f; - this->accumulated_alpha_ = 0.0f; - if (this->is_effect_active()) return; // don't use LightState helper, gamma correction+brightness is handled by ESPColorView - - if (state->transformer_ == nullptr || !state->transformer_->is_transition()) { - // no transformer active or non-transition one - this->all() = esp_color_from_light_color_values(val); - } else { - // transition transformer active, activate specialized transition for addressable effects - // instead of using a unified transition for all LEDs, we use the current state each LED as the - // start. Warning: ugly - - // We can't use a direct lerp smoothing here though - that would require creating a copy of the original - // state of each LED at the start of the transition - // Instead, we "fake" the look of the LERP by using an exponential average over time and using - // dynamically-calculated alpha values to match the look of the - - float new_progress = state->transformer_->get_progress(); - float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_); - float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress); - this->last_transition_progress_ = new_progress; - - auto end_values = state->transformer_->get_end_values(); - Color target_color = esp_color_from_light_color_values(end_values); - - // our transition will handle brightness, disable brightness in correction. - this->correction_.set_local_brightness(255); - uint8_t orig_w = target_color.w; - target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); - // w is not scaled by brightness - target_color.w = orig_w; - - float denom = (1.0f - new_smoothed); - float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; - - // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length - // We solve this by accumulating the fractional part of the alpha over time. - float alpha255 = alpha * 255.0f; - float alpha255int = floorf(alpha255); - float alpha255remainder = alpha255 - alpha255int; - - this->accumulated_alpha_ += alpha255remainder; - float alpha_add = floorf(this->accumulated_alpha_); - this->accumulated_alpha_ -= alpha_add; - - alpha255 += alpha_add; - alpha255 = clamp(alpha255, 0.0f, 255.0f); - auto alpha8 = static_cast(alpha255); - - if (alpha8 != 0) { - uint8_t inv_alpha8 = 255 - alpha8; - Color add = target_color * alpha8; - - for (auto led : *this) - led = add + led.get() * inv_alpha8; - } - } - + this->all() = color_from_light_color_values(val); this->schedule_show(); } -void ESPColorCorrection::calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; +void AddressableLightTransformer::start() { + // don't try to transition over running effects. + if (this->light_.is_effect_active()) return; + + auto end_values = this->target_values_; + this->target_color_ = color_from_light_color_values(end_values); + + // our transition will handle brightness, disable brightness in correction. + this->light_.correction_.set_local_brightness(255); + this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); +} + +optional AddressableLightTransformer::apply() { + float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_()); + + // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the + // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the + // effects which respect it. + if (this->light_.is_effect_active()) + return LightColorValues::lerp(this->get_start_values(), this->get_target_values(), smoothed_progress); + + // Use a specialized transition for addressable lights: instead of using a unified transition for + // all LEDs, we use the current state of each LED as the start. + + // We can't use a direct lerp smoothing here though - that would require creating a copy of the original + // state of each LED at the start of the transition. + // Instead, we "fake" the look of the LERP by using an exponential average over time and using + // dynamically-calculated alpha values to match the look. + + float denom = (1.0f - smoothed_progress); + float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom; + + // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length + // We solve this by accumulating the fractional part of the alpha over time. + float alpha255 = alpha * 255.0f; + float alpha255int = floorf(alpha255); + float alpha255remainder = alpha255 - alpha255int; + + this->accumulated_alpha_ += alpha255remainder; + float alpha_add = floorf(this->accumulated_alpha_); + this->accumulated_alpha_ -= alpha_add; + + alpha255 += alpha_add; + alpha255 = clamp(alpha255, 0.0f, 255.0f); + auto alpha8 = static_cast(alpha255); + + if (alpha8 != 0) { + uint8_t inv_alpha8 = 255 - alpha8; + Color add = this->target_color_ * alpha8; + + for (auto led : this->light_) + led.set(add + led.get() * inv_alpha8); } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); - this->gamma_reverse_table_[i] = uncorrected; - } + + this->last_transition_progress_ = smoothed_progress; + this->light_.schedule_show(); + + return {}; } } // namespace light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 39bd905c65..fea7508515 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -3,8 +3,12 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/color.h" +#include "esp_color_correction.h" +#include "esp_color_view.h" +#include "esp_range_view.h" #include "light_output.h" #include "light_state.h" +#include "transformers.h" #ifdef USE_POWER_SUPPLY #include "esphome/components/power_supply/power_supply.h" @@ -13,267 +17,10 @@ namespace esphome { namespace light { -using ESPColor = Color; +using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color; -struct ESPHSVColor { - union { - struct { - union { - uint8_t hue; - uint8_t h; - }; - union { - uint8_t saturation; - uint8_t s; - }; - union { - uint8_t value; - uint8_t v; - }; - }; - uint8_t raw[3]; - }; - inline ESPHSVColor() 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) {} - Color to_rgb() const; -}; - -class ESPColorCorrection { - public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} - 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 { - // 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 { - 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 { - 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 { - 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 { - // do not scale white value with brightness - uint8_t res = esp_scale8(white, this->max_brightness_.white); - return this->gamma_table_[res]; - } - inline Color color_uncorrect(Color color) const 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 { - 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; - } - inline uint8_t color_uncorrect_green(uint8_t green) const 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; - } - inline uint8_t color_uncorrect_blue(uint8_t blue) const 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; - } - inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { - if (this->max_brightness_.white == 0) - return 0; - uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = uncorrected / this->max_brightness_.white; - return res; - } - - protected: - uint8_t gamma_table_[256]; - uint8_t gamma_reverse_table_[256]; - Color max_brightness_; - uint8_t local_brightness_{255}; -}; - -class ESPColorSettable { - public: - virtual void set(const Color &color) = 0; - virtual void set_red(uint8_t red) = 0; - virtual void set_green(uint8_t green) = 0; - virtual void set_blue(uint8_t blue) = 0; - virtual void set_white(uint8_t white) = 0; - virtual void set_effect_data(uint8_t effect_data) = 0; - virtual void fade_to_white(uint8_t amnt) = 0; - virtual void fade_to_black(uint8_t amnt) = 0; - virtual void lighten(uint8_t delta) = 0; - virtual void darken(uint8_t delta) = 0; - void set(const ESPHSVColor &color) { this->set_hsv(color); } - void set_hsv(const ESPHSVColor &color) { - Color rgb = color.to_rgb(); - this->set_rgb(rgb.r, rgb.g, rgb.b); - } - void set_rgb(uint8_t red, uint8_t green, uint8_t blue) { - this->set_red(red); - this->set_green(green); - this->set_blue(blue); - } - void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) { - this->set_rgb(red, green, blue); - this->set_white(white); - } -}; - -class ESPColorView : public ESPColorSettable { - public: - ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, - const ESPColorCorrection *color_correction) - : red_(red), - green_(green), - blue_(blue), - white_(white), - effect_data_(effect_data), - color_correction_(color_correction) {} - ESPColorView &operator=(const Color &rhs) { - this->set(rhs); - return *this; - } - ESPColorView &operator=(const ESPHSVColor &rhs) { - this->set_hsv(rhs); - return *this; - } - void set(const Color &color) override { this->set_rgbw(color.r, color.g, color.b, color.w); } - void set_red(uint8_t red) override { *this->red_ = this->color_correction_->color_correct_red(red); } - void set_green(uint8_t green) override { *this->green_ = this->color_correction_->color_correct_green(green); } - void set_blue(uint8_t blue) override { *this->blue_ = this->color_correction_->color_correct_blue(blue); } - void set_white(uint8_t white) override { - if (this->white_ == nullptr) - return; - *this->white_ = this->color_correction_->color_correct_white(white); - } - void set_effect_data(uint8_t effect_data) override { - if (this->effect_data_ == nullptr) - return; - *this->effect_data_ = effect_data; - } - void fade_to_white(uint8_t amnt) override { this->set(this->get().fade_to_white(amnt)); } - void fade_to_black(uint8_t amnt) override { this->set(this->get().fade_to_black(amnt)); } - void lighten(uint8_t delta) override { this->set(this->get().lighten(delta)); } - void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } - Color get() const { return Color(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } - uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } - uint8_t get_red_raw() const { return *this->red_; } - uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } - uint8_t get_green_raw() const { return *this->green_; } - uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } - uint8_t get_blue_raw() const { return *this->blue_; } - uint8_t get_white() const { - if (this->white_ == nullptr) - return 0; - return this->color_correction_->color_uncorrect_white(*this->white_); - } - uint8_t get_white_raw() const { - if (this->white_ == nullptr) - return 0; - return *this->white_; - } - uint8_t get_effect_data() const { - if (this->effect_data_ == nullptr) - return 0; - return *this->effect_data_; - } - void raw_set_color_correction(const ESPColorCorrection *color_correction) { - this->color_correction_ = color_correction; - } - - protected: - uint8_t *const red_; - uint8_t *const green_; - uint8_t *const blue_; - uint8_t *const white_; - uint8_t *const effect_data_; - const ESPColorCorrection *color_correction_; -}; - -class AddressableLight; - -int32_t interpret_index(int32_t index, int32_t size); - -class ESPRangeIterator; - -class ESPRangeView : public ESPColorSettable { - public: - ESPRangeView(AddressableLight *parent, int32_t begin, int32_t an_end) : parent_(parent), begin_(begin), end_(an_end) { - if (this->end_ < this->begin_) { - this->end_ = this->begin_; - } - } - - ESPColorView operator[](int32_t index) const; - ESPRangeIterator begin(); - ESPRangeIterator end(); - - void set(const Color &color) override; - ESPRangeView &operator=(const Color &rhs) { - this->set(rhs); - return *this; - } - ESPRangeView &operator=(const ESPColorView &rhs) { - this->set(rhs.get()); - return *this; - } - ESPRangeView &operator=(const ESPHSVColor &rhs) { - this->set_hsv(rhs); - return *this; - } - ESPRangeView &operator=(const ESPRangeView &rhs); - void set_red(uint8_t red) override; - void set_green(uint8_t green) override; - void set_blue(uint8_t blue) override; - void set_white(uint8_t white) override; - void set_effect_data(uint8_t effect_data) override; - void fade_to_white(uint8_t amnt) override; - void fade_to_black(uint8_t amnt) override; - void lighten(uint8_t delta) override; - void darken(uint8_t delta) override; - int32_t size() const { return this->end_ - this->begin_; } - - protected: - friend ESPRangeIterator; - - AddressableLight *parent_; - int32_t begin_; - int32_t end_; -}; - -class ESPRangeIterator { - public: - ESPRangeIterator(const ESPRangeView &range, int32_t i) : range_(range), i_(i) {} - ESPRangeIterator operator++() { - this->i_++; - return *this; - } - bool operator!=(const ESPRangeIterator &other) const { return this->i_ != other.i_; } - ESPColorView operator*() const; - - protected: - ESPRangeView range_; - int32_t i_; -}; +/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). +Color color_from_light_color_values(LightColorValues val); class AddressableLight : public LightOutput, public Component { public: @@ -307,18 +54,20 @@ class AddressableLight : public LightOutput, public Component { amnt = this->size(); this->range(amnt, this->size()) = this->range(0, -amnt); } + // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } - void write_state(LightState *state) override; + std::unique_ptr create_default_transition() override; void set_correction(float red, float green, float blue, float white = 1.0f) { - this->correction_.set_max_brightness(Color(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), - uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); + this->correction_.set_max_brightness( + Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { this->correction_.calculate_gamma_table(state->get_gamma_correct()); this->state_parent_ = state; } - void schedule_show() { this->next_show_ = true; } + void update_state(LightState *state) override; + void schedule_show() { this->state_parent_->next_write_ = true; } #ifdef USE_POWER_SUPPLY void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } @@ -327,11 +76,11 @@ class AddressableLight : public LightOutput, public Component { void call_setup() override; protected: - bool should_show_() const { return this->effect_active_ || this->next_show_; } + friend class AddressableLightTransformer; + void mark_shown_() { - this->next_show_ = false; #ifdef USE_POWER_SUPPLY - for (auto c : *this) { + for (const auto &c : *this) { if (c.get().is_on()) { this->power_.request(); return; @@ -343,12 +92,23 @@ class AddressableLight : public LightOutput, public Component { virtual ESPColorView get_view_internal(int32_t index) const = 0; bool effect_active_{false}; - bool next_show_{true}; ESPColorCorrection correction_{}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; #endif LightState *state_parent_{nullptr}; +}; + +class AddressableLightTransformer : public LightTransitionTransformer { + public: + AddressableLightTransformer(AddressableLight &light) : light_(light) {} + + void start() override; + optional apply() override; + + protected: + AddressableLight &light_; + Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; }; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index d1ea9e3ff0..358fe69c23 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -38,11 +38,8 @@ class AddressableLightEffect : public LightEffect { void stop() override { this->get_addressable_()->set_effect_active(false); } virtual void apply(AddressableLight &it, const Color ¤t_color) = 0; void apply() override { - LightColorValues color = this->state_->remote_values; - // not using any color correction etc. that will be handled by the addressable layer - Color current_color = - Color(static_cast(color.get_red() * 255), static_cast(color.get_green() * 255), - static_cast(color.get_blue() * 255), static_cast(color.get_white() * 255)); + // not using any color correction etc. that will be handled by the addressable layer through ESPColorCorrection + Color current_color = color_from_light_color_values(this->state_->remote_values); this->apply(*this->get_addressable_(), current_color); } @@ -63,6 +60,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; + it.schedule_show(); } } @@ -87,6 +85,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect { var = hsv; hue += add; } + it.schedule_show(); } void set_speed(uint32_t speed) { this->speed_ = speed; } void set_width(uint16_t width) { this->width_ = width; } @@ -134,6 +133,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { new_color.b = c.b; } } + it.schedule_show(); } protected: @@ -151,25 +151,27 @@ class AddressableScanEffect : public AddressableLightEffect { void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { - it.all() = COLOR_BLACK; + const uint32_t now = millis(); + if (now - this->last_move_ < this->move_interval_) + return; + if (direction_) { + this->at_led_++; + if (this->at_led_ == it.size() - this->scan_width_) + this->direction_ = false; + } else { + this->at_led_--; + if (this->at_led_ == 0) + this->direction_ = true; + } + this->last_move_ = now; + + it.all() = Color::BLACK; for (auto i = 0; i < this->scan_width_; i++) { it[this->at_led_ + i] = current_color; } - const uint32_t now = millis(); - if (now - this->last_move_ > this->move_interval_) { - if (direction_) { - this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) - this->direction_ = false; - } else { - this->at_led_--; - if (this->at_led_ == 0) - this->direction_ = true; - } - this->last_move_ = now; - } + it.schedule_show(); } protected: @@ -201,7 +203,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { else view.set_effect_data(new_pos); } else { - view = COLOR_BLACK; + view = Color::BLACK; } } while (random_float() < this->twinkle_probability_) { @@ -210,6 +212,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { continue; addressable[pos].set_effect_data(1); } + addressable.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -257,6 +260,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { const uint8_t color = random_uint32() & 0b111; it[pos].set_effect_data(0b1000 | color); } + it.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -272,7 +276,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); - it.all() = COLOR_BLACK; + it.all() = Color::BLACK; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); @@ -301,6 +305,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { it[pos] = current_color; } } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; } @@ -335,9 +340,10 @@ class AddressableFlickerEffect : public AddressableLightEffect { // slowly fade back to "real" value var = (var.get() * inv_intensity) + (current_color * intensity); } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } - void set_intensity(float intensity) { this->intensity_ = static_cast(roundf(intensity * 255.0f)); } + void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } protected: uint32_t update_interval_{16}; diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h new file mode 100644 index 0000000000..cd5bcabd47 --- /dev/null +++ b/esphome/components/light/addressable_light_wrapper.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/component.h" +#include "addressable_light.h" + +namespace esphome { +namespace light { + +class AddressableLightWrapper : public light::AddressableLight { + public: + explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) { + this->wrapper_state_ = new uint8_t[5]; // NOLINT(cppcoreguidelines-owning-memory) + } + + int32_t size() const override { return 1; } + + void clear_effect_data() override { this->wrapper_state_[4] = 0; } + + light::LightTraits get_traits() override { return this->light_state_->get_traits(); } + + void write_state(light::LightState *state) override { + float gamma = this->light_state_->get_gamma_correct(); + float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma); + float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma); + float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma); + float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma); + float brightness = fmaxf(r, fmaxf(g, b)); + + auto call = this->light_state_->make_call(); + call.set_state(true); + call.set_brightness_if_supported(1.0f); + call.set_color_brightness_if_supported(brightness); + call.set_red_if_supported(r); + call.set_green_if_supported(g); + call.set_blue_if_supported(b); + call.set_white_if_supported(w); + call.set_transition_length_if_supported(0); + call.set_publish(false); + call.set_save(false); + call.perform(); + + this->mark_shown_(); + } + + protected: + light::ESPColorView get_view_internal(int32_t index) const override { + return {&this->wrapper_state_[0], &this->wrapper_state_[1], &this->wrapper_state_[2], + &this->wrapper_state_[3], &this->wrapper_state_[4], &this->correction_}; + } + + light::LightState *light_state_; + uint8_t *wrapper_state_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp new file mode 100644 index 0000000000..8c1785f061 --- /dev/null +++ b/esphome/components/light/automation.cpp @@ -0,0 +1,15 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +static const char *const TAG = "light.automation"; + +void addressableset_warn_about_scale(const char *field) { + ESP_LOGW(TAG, "Lambda for parameter %s of light.addressable_set should return values in range 0-1 instead of 0-255.", + field); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index d1fb2a0bcb..5ec2cb626a 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -27,26 +27,34 @@ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(ColorMode, color_mode) TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) TEMPLATABLE_VALUE(float, brightness) + TEMPLATABLE_VALUE(float, color_brightness) TEMPLATABLE_VALUE(float, red) TEMPLATABLE_VALUE(float, green) TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) TEMPLATABLE_VALUE(float, color_temperature) + TEMPLATABLE_VALUE(float, cold_white) + TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) void play(Ts... x) override { auto call = this->parent_->make_call(); + call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); + call.set_color_brightness(this->color_brightness_.optional_value(x...)); call.set_red(this->red_.optional_value(x...)); call.set_green(this->green_.optional_value(x...)); call.set_blue(this->blue_.optional_value(x...)); call.set_white(this->white_.optional_value(x...)); call.set_color_temperature(this->color_temperature_.optional_value(x...)); + call.set_cold_white(this->cold_white_.optional_value(x...)); + call.set_warm_white(this->warm_white_.optional_value(x...)); call.set_effect(this->effect_.optional_value(x...)); call.set_flash_length(this->flash_length_.optional_value(x...)); call.set_transition_length(this->transition_length_.optional_value(x...)); @@ -133,35 +141,57 @@ class LightTurnOffTrigger : public Trigger<> { } }; +// This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet +// due to the template. It's just a temporary warning anyway. +void addressableset_warn_about_scale(const char *field); + template class AddressableSet : public Action { public: explicit AddressableSet(LightState *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int32_t, range_from) TEMPLATABLE_VALUE(int32_t, range_to) - TEMPLATABLE_VALUE(uint8_t, red) - TEMPLATABLE_VALUE(uint8_t, green) - TEMPLATABLE_VALUE(uint8_t, blue) - TEMPLATABLE_VALUE(uint8_t, white) + TEMPLATABLE_VALUE(float, color_brightness) + TEMPLATABLE_VALUE(float, red) + TEMPLATABLE_VALUE(float, green) + TEMPLATABLE_VALUE(float, blue) + TEMPLATABLE_VALUE(float, white) void play(Ts... x) override { auto *out = (AddressableLight *) this->parent_->get_output(); - int32_t range_from = this->range_from_.value_or(x..., 0); - int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; + int32_t range_from = interpret_index(this->range_from_.value_or(x..., 0), out->size()); + if (range_from < 0 || range_from >= out->size()) + range_from = 0; + + int32_t range_to = interpret_index(this->range_to_.value_or(x..., out->size() - 1) + 1, out->size()); + if (range_to < 0 || range_to >= out->size()) + range_to = out->size(); + + uint8_t color_brightness = + to_uint8_scale(this->color_brightness_.value_or(x..., this->parent_->remote_values.get_color_brightness())); auto range = out->range(range_from, range_to); if (this->red_.has_value()) - range.set_red(this->red_.value(x...)); + range.set_red(esp_scale8(to_uint8_compat(this->red_.value(x...), "red"), color_brightness)); if (this->green_.has_value()) - range.set_green(this->green_.value(x...)); + range.set_green(esp_scale8(to_uint8_compat(this->green_.value(x...), "green"), color_brightness)); if (this->blue_.has_value()) - range.set_blue(this->blue_.value(x...)); + range.set_blue(esp_scale8(to_uint8_compat(this->blue_.value(x...), "blue"), color_brightness)); if (this->white_.has_value()) - range.set_white(this->white_.value(x...)); + range.set_white(to_uint8_compat(this->white_.value(x...), "white")); out->schedule_show(); } protected: LightState *parent_; + + // Historically, this action required uint8_t (0-255) for RGBW values from lambdas. Keep compatibility. + static inline uint8_t to_uint8_compat(float value, const char *field) { + if (value > 1.0f) { + addressableset_warn_about_scale(field); + return static_cast(value); + } + return to_uint8_scale(value); + } }; } // namespace light diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 3fb3126f14..cfba273565 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -3,20 +3,26 @@ 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_COLD_WHITE, + CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, ) from .types import ( + ColorMode, + COLOR_MODES, DimRelativeAction, ToggleAction, LightState, @@ -54,6 +60,7 @@ async def light_toggle_to_code(config, action_id, template_arg, args): LIGHT_CONTROL_ACTION_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 @@ -63,11 +70,14 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( ), 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), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), + cv.Optional(CONF_COLD_WHITE): cv.templatable(cv.percentage), + cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage), } ) LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( @@ -100,6 +110,9 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def light_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_COLOR_MODE in config: + template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) + cg.add(var.set_color_mode(template_)) if CONF_STATE in config: template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) @@ -114,6 +127,9 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_BRIGHTNESS in config: template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) cg.add(var.set_brightness(template_)) + if CONF_COLOR_BRIGHTNESS in config: + template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) + cg.add(var.set_color_brightness(template_)) if CONF_RED in config: template_ = await cg.templatable(config[CONF_RED], args, float) cg.add(var.set_red(template_)) @@ -129,6 +145,12 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_COLOR_TEMPERATURE in config: template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) cg.add(var.set_color_temperature(template_)) + if CONF_COLD_WHITE in config: + template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) + cg.add(var.set_cold_white(template_)) + if CONF_WARM_WHITE in config: + template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) + cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) cg.add(var.set_effect(template_)) @@ -168,6 +190,7 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema( cv.Required(CONF_ID): cv.use_id(AddressableLightState), cv.Optional(CONF_RANGE_FROM): cv.templatable(cv.positive_int), cv.Optional(CONF_RANGE_TO): cv.templatable(cv.positive_int), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), @@ -189,28 +212,20 @@ async def light_addressable_set_to_code(config, action_id, template_arg, args): templ = await cg.templatable(config[CONF_RANGE_TO], args, cg.int32) cg.add(var.set_range_to(templ)) - def rgbw_to_exp(x): - return int(round(x * 255)) - + if CONF_COLOR_BRIGHTNESS in config: + templ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, cg.float_) + cg.add(var.set_color_brightness(templ)) if CONF_RED in config: - templ = await cg.templatable( - config[CONF_RED], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_RED], args, cg.float_) cg.add(var.set_red(templ)) if CONF_GREEN in config: - templ = await cg.templatable( - config[CONF_GREEN], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_GREEN], args, cg.float_) cg.add(var.set_green(templ)) if CONF_BLUE in config: - templ = await cg.templatable( - config[CONF_BLUE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_BLUE], args, cg.float_) cg.add(var.set_blue(templ)) if CONF_WHITE in config: - templ = await cg.templatable( - config[CONF_WHITE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_WHITE], args, cg.float_) cg.add(var.set_white(templ)) return var diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index eb92fec642..5ab9f66ce4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -57,16 +57,31 @@ class RandomLightEffect : public LightEffect { if (now - this->last_color_change_ < this->update_interval_) { return; } + + auto color_mode = this->state_->remote_values.get_color_mode(); auto call = this->state_->turn_on(); - if (this->state_->get_traits().get_supports_rgb()) { - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); - } else { - call.set_brightness_if_supported(random_float()); + bool changed = false; + if (color_mode & ColorCapability::RGB) { + call.set_red(random_float()); + call.set_green(random_float()); + call.set_blue(random_float()); + changed = true; + } + if (color_mode & ColorCapability::COLOR_TEMPERATURE) { + float min = this->state_->get_traits().get_min_mireds(); + float max = this->state_->get_traits().get_max_mireds(); + call.set_color_temperature(min + random_float() * (max - min)); + changed = true; + } + if (color_mode & ColorCapability::COLD_WARM_WHITE) { + call.set_cold_white(random_float()); + call.set_warm_white(random_float()); + changed = true; + } + if (!changed) { + // only randomize brightness if there's no colored option available + call.set_brightness(random_float()); } - call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); call.set_save(false); @@ -141,8 +156,7 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped - call.set_brightness_if_supported(0.0f); - call.set_white_if_supported(0.0f); + call.set_brightness(0.0f); call.set_state(true); } call.set_publish(false); @@ -177,13 +191,15 @@ class FlickerLightEffect : public LightEffect { out.set_green(remote.get_green() * beta + current.get_green() * alpha + (random_cubic_float() * this->intensity_)); out.set_blue(remote.get_blue() * beta + current.get_blue() * alpha + (random_cubic_float() * this->intensity_)); out.set_white(remote.get_white() * beta + current.get_white() * alpha + (random_cubic_float() * this->intensity_)); + out.set_cold_white(remote.get_cold_white() * beta + current.get_cold_white() * alpha + + (random_cubic_float() * this->intensity_)); + out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + + (random_cubic_float() * this->intensity_)); - auto traits = this->state_->get_traits(); auto call = this->state_->make_call(); call.set_publish(false); call.set_save(false); - if (traits.get_supports_brightness()) - call.set_transition_length(0); + call.set_transition_length_if_supported(0); call.from_light_color_values(out); call.set_state(true); call.perform(); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h new file mode 100644 index 0000000000..77c377d39e --- /dev/null +++ b/esphome/components/light/color_mode.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +namespace esphome { +namespace light { + +/// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. +enum class ColorCapability : uint8_t { + /// Light can be turned on/off. + ON_OFF = 1 << 0, + /// Master brightness of the light can be controlled. + BRIGHTNESS = 1 << 1, + /// Brightness of white channel can be controlled separately from other channels. + WHITE = 1 << 2, + /// Color temperature can be controlled. + COLOR_TEMPERATURE = 1 << 3, + /// Brightness of cold and warm white output can be controlled. + COLD_WARM_WHITE = 1 << 4, + /// Color can be controlled using RGB format (includes a brightness control for the color). + RGB = 1 << 5 +}; + +/// Helper class to allow bitwise operations on ColorCapability +class ColorCapabilityHelper { + public: + constexpr ColorCapabilityHelper(ColorCapability val) : val_(val) {} + constexpr operator ColorCapability() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorCapability val_; +}; +constexpr ColorCapabilityHelper operator&(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator&(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/// Color modes are a combination of color capabilities that can be used at the same time. +enum class ColorMode : uint8_t { + /// No color mode configured (cannot be a supported mode, only active when light is off). + UNKNOWN = 0, + /// Only on/off control. + ON_OFF = (uint8_t) ColorCapability::ON_OFF, + /// Dimmable light. + BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS), + /// White output only (use only if the light also has another color mode such as RGB). + WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), + /// Controllable color temperature output. + COLOR_TEMPERATURE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLOR_TEMPERATURE), + /// Cold and warm white output with individually controllable brightness. + COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLD_WARM_WHITE), + /// RGB color output. + RGB = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB), + /// RGB color output and a separate white output. + RGB_WHITE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | ColorCapability::WHITE), + /// RGB color output and a separate white output with controllable color temperature. + RGB_COLOR_TEMPERATURE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::WHITE | ColorCapability::COLOR_TEMPERATURE), + /// RGB color output, and separate cold and warm white outputs. + RGB_COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::COLD_WARM_WHITE), +}; + +/// Helper class to allow bitwise operations on ColorMode with ColorCapability +class ColorModeHelper { + public: + constexpr ColorModeHelper(ColorMode val) : val_(val) {} + constexpr operator ColorMode() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorMode val_; +}; +constexpr ColorModeHelper operator&(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c213de0ae6..4b2209c833 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,10 +12,15 @@ from esphome.const import ( 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_INTENSITY, CONF_SPEED, @@ -26,6 +31,8 @@ from esphome.const import ( ) from esphome.util import Registry from .types import ( + ColorMode, + COLOR_MODES, LambdaLightEffect, PulseLightEffect, RandomLightEffect, @@ -211,10 +218,17 @@ async def random_effect_to_code(config, effect_id): { cv.Optional(CONF_STATE, default=True): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_MODE): cv.enum( + COLOR_MODES, upper=True, space="_" + ), + cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, cv.Optional(CONF_BLUE, default=1.0): cv.percentage, cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_WARM_WHITE, default=1.0): cv.percentage, cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, @@ -223,10 +237,15 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, 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, ), ), cv.Length(min=2), @@ -243,12 +262,17 @@ async def strobe_effect_to_code(config, effect_id): ( "color", LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), color[CONF_STATE], color[CONF_BRIGHTNESS], + color[CONF_COLOR_BRIGHTNESS], color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), ), ("duration", color[CONF_DURATION]), @@ -466,8 +490,7 @@ def validate_effects(allowed_effects): if key not in allowed_effects: errors.append( cv.Invalid( - "The effect '{}' is not allowed for this " - "light type".format(key), + f"The effect '{key}' is not allowed for this light type", [i], ) ) @@ -476,8 +499,7 @@ def validate_effects(allowed_effects): if name in names: errors.append( cv.Invalid( - "Found the effect name '{}' twice. All effects must have " - "unique names".format(name), + f"Found the effect name '{name}' twice. All effects must have unique names", [i], ) ) diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp new file mode 100644 index 0000000000..e5e68264cc --- /dev/null +++ b/esphome/components/light/esp_color_correction.cpp @@ -0,0 +1,27 @@ +#include "esp_color_correction.h" +#include "light_color_values.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +void ESPColorCorrection::calculate_gamma_table(float gamma) { + for (uint16_t i = 0; i < 256; i++) { + // corrected = val ^ gamma + auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); + this->gamma_table_[i] = corrected; + } + if (gamma == 0.0f) { + for (uint16_t i = 0; i < 256; i++) + this->gamma_reverse_table_[i] = i; + return; + } + for (uint16_t i = 0; i < 256; i++) { + // val = corrected ^ (1/gamma) + auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); + this->gamma_reverse_table_[i] = uncorrected; + } +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h new file mode 100644 index 0000000000..8788246cfc --- /dev/null +++ b/esphome/components/light/esp_color_correction.h @@ -0,0 +1,77 @@ +#pragma once + +#include "esphome/core/color.h" + +namespace esphome { +namespace light { + +class ESPColorCorrection { + public: + ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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; + } + inline uint8_t color_uncorrect_green(uint8_t green) const 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; + } + inline uint8_t color_uncorrect_blue(uint8_t blue) const 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; + } + inline uint8_t color_uncorrect_white(uint8_t white) const 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; + } + + protected: + uint8_t gamma_table_[256]; + uint8_t gamma_reverse_table_[256]; + Color max_brightness_; + uint8_t local_brightness_{255}; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h new file mode 100644 index 0000000000..35117e7dd8 --- /dev/null +++ b/esphome/components/light/esp_color_view.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esp_hsv_color.h" +#include "esp_color_correction.h" + +namespace esphome { +namespace light { + +class ESPColorSettable { + public: + virtual void set(const Color &color) = 0; + virtual void set_red(uint8_t red) = 0; + virtual void set_green(uint8_t green) = 0; + virtual void set_blue(uint8_t blue) = 0; + virtual void set_white(uint8_t white) = 0; + virtual void set_effect_data(uint8_t effect_data) = 0; + virtual void fade_to_white(uint8_t amnt) = 0; + virtual void fade_to_black(uint8_t amnt) = 0; + virtual void lighten(uint8_t delta) = 0; + virtual void darken(uint8_t delta) = 0; + void set(const ESPHSVColor &color) { this->set_hsv(color); } + void set_hsv(const ESPHSVColor &color) { + Color rgb = color.to_rgb(); + this->set_rgb(rgb.r, rgb.g, rgb.b); + } + void set_rgb(uint8_t red, uint8_t green, uint8_t blue) { + this->set_red(red); + this->set_green(green); + this->set_blue(blue); + } + void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) { + this->set_rgb(red, green, blue); + this->set_white(white); + } +}; + +class ESPColorView : public ESPColorSettable { + public: + ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, + const ESPColorCorrection *color_correction) + : red_(red), + green_(green), + blue_(blue), + white_(white), + effect_data_(effect_data), + color_correction_(color_correction) {} + ESPColorView &operator=(const Color &rhs) { + this->set(rhs); + return *this; + } + ESPColorView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + void set(const Color &color) override { this->set_rgbw(color.r, color.g, color.b, color.w); } + void set_red(uint8_t red) override { *this->red_ = this->color_correction_->color_correct_red(red); } + void set_green(uint8_t green) override { *this->green_ = this->color_correction_->color_correct_green(green); } + void set_blue(uint8_t blue) override { *this->blue_ = this->color_correction_->color_correct_blue(blue); } + void set_white(uint8_t white) override { + if (this->white_ == nullptr) + return; + *this->white_ = this->color_correction_->color_correct_white(white); + } + void set_effect_data(uint8_t effect_data) override { + if (this->effect_data_ == nullptr) + return; + *this->effect_data_ = effect_data; + } + void fade_to_white(uint8_t amnt) override { this->set(this->get().fade_to_white(amnt)); } + void fade_to_black(uint8_t amnt) override { this->set(this->get().fade_to_black(amnt)); } + void lighten(uint8_t delta) override { this->set(this->get().lighten(delta)); } + void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } + Color get() const { return Color(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } + uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } + uint8_t get_red_raw() const { return *this->red_; } + uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } + uint8_t get_green_raw() const { return *this->green_; } + uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } + uint8_t get_blue_raw() const { return *this->blue_; } + uint8_t get_white() const { + if (this->white_ == nullptr) + return 0; + return this->color_correction_->color_uncorrect_white(*this->white_); + } + uint8_t get_white_raw() const { + if (this->white_ == nullptr) + return 0; + return *this->white_; + } + uint8_t get_effect_data() const { + if (this->effect_data_ == nullptr) + return 0; + return *this->effect_data_; + } + void raw_set_color_correction(const ESPColorCorrection *color_correction) { + this->color_correction_ = color_correction; + } + + protected: + uint8_t *const red_; + uint8_t *const green_; + uint8_t *const blue_; + uint8_t *const white_; + uint8_t *const effect_data_; + const ESPColorCorrection *color_correction_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp new file mode 100644 index 0000000000..450c2e11ce --- /dev/null +++ b/esphome/components/light/esp_hsv_color.cpp @@ -0,0 +1,74 @@ +#include "esp_hsv_color.h" + +namespace esphome { +namespace light { + +Color ESPHSVColor::to_rgb() const { + // based on FastLED's hsv rainbow to rgb + const uint8_t hue = this->hue; + const uint8_t sat = this->saturation; + const uint8_t val = this->value; + // upper 3 hue bits are for branch selection, lower 5 are for values + const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 + // third of the offset, 255/3 = 85 (actually only up to 82; 164) + const uint8_t third = esp_scale8(offset8, 85); + const uint8_t two_thirds = esp_scale8(offset8, 170); + Color rgb(255, 255, 255, 0); + switch (hue >> 5) { + case 0b000: + rgb.r = 255 - third; + rgb.g = third; + rgb.b = 0; + break; + case 0b001: + rgb.r = 171; + rgb.g = 85 + third; + rgb.b = 0; + break; + case 0b010: + rgb.r = 171 - two_thirds; + rgb.g = 170 + third; + rgb.b = 0; + break; + case 0b011: + rgb.r = 0; + rgb.g = 255 - third; + rgb.b = third; + break; + case 0b100: + rgb.r = 0; + rgb.g = 171 - two_thirds; + rgb.b = 85 + two_thirds; + break; + case 0b101: + rgb.r = third; + rgb.g = 0; + rgb.b = 255 - third; + break; + case 0b110: + rgb.r = 85 + third; + rgb.g = 0; + rgb.b = 171 - third; + break; + case 0b111: + rgb.r = 170 + third; + rgb.g = 0; + rgb.b = 85 - third; + break; + default: + break; + } + // low saturation -> add uniform color to orig. hue + // high saturation -> use hue directly + // scales with square of saturation + // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 + rgb *= sat; + const uint8_t desat = 255 - sat; + rgb += esp_scale8(desat, desat); + // (r,g,b) = (r,g,b) * val + rgb *= val; + return rgb; +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h new file mode 100644 index 0000000000..e0aa388875 --- /dev/null +++ b/esphome/components/light/esp_hsv_color.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/color.h" + +namespace esphome { +namespace light { + +struct ESPHSVColor { + union { + struct { + union { + uint8_t hue; + uint8_t h; + }; + union { + uint8_t saturation; + uint8_t s; + }; + union { + uint8_t value; + uint8_t v; + }; + }; + uint8_t raw[3]; + }; + inline ESPHSVColor() 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) {} + Color to_rgb() const; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp new file mode 100644 index 0000000000..e1f0a507bd --- /dev/null +++ b/esphome/components/light/esp_range_view.cpp @@ -0,0 +1,96 @@ +#include "esp_range_view.h" +#include "addressable_light.h" + +namespace esphome { +namespace light { + +int32_t HOT interpret_index(int32_t index, int32_t size) { + if (index < 0) + return size + index; + return index; +} + +ESPColorView ESPRangeView::operator[](int32_t index) const { + index = interpret_index(index, this->size()) + this->begin_; + return (*this->parent_)[index]; +} +ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } +ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } + +void ESPRangeView::set(const Color &color) { + for (int32_t i = this->begin_; i < this->end_; i++) { + (*this->parent_)[i] = color; + } +} + +void ESPRangeView::set_red(uint8_t red) { + for (auto c : *this) + c.set_red(red); +} +void ESPRangeView::set_green(uint8_t green) { + for (auto c : *this) + c.set_green(green); +} +void ESPRangeView::set_blue(uint8_t blue) { + for (auto c : *this) + c.set_blue(blue); +} +void ESPRangeView::set_white(uint8_t white) { + for (auto c : *this) + c.set_white(white); +} +void ESPRangeView::set_effect_data(uint8_t effect_data) { + for (auto c : *this) + c.set_effect_data(effect_data); +} + +void ESPRangeView::fade_to_white(uint8_t amnt) { + for (auto c : *this) + c.fade_to_white(amnt); +} +void ESPRangeView::fade_to_black(uint8_t amnt) { + for (auto c : *this) + c.fade_to_black(amnt); +} +void ESPRangeView::lighten(uint8_t delta) { + for (auto c : *this) + c.lighten(delta); +} +void ESPRangeView::darken(uint8_t delta) { + for (auto c : *this) + c.darken(delta); +} +ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT + // If size doesn't match, error (todo warning) + if (rhs.size() != this->size()) + return *this; + + if (this->parent_ != rhs.parent_) { + for (int32_t i = 0; i < this->size(); i++) + (*this)[i].set(rhs[i].get()); + return *this; + } + + // If both equal, already done + if (rhs.begin_ == this->begin_) + return *this; + + if (rhs.begin_ > this->begin_) { + // Copy from left + for (int32_t i = 0; i < this->size(); i++) { + (*this)[i].set(rhs[i].get()); + } + } else { + // Copy from right + for (int32_t i = this->size() - 1; i >= 0; i--) { + (*this)[i].set(rhs[i].get()); + } + } + + return *this; +} + +ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h new file mode 100644 index 0000000000..07d18af79f --- /dev/null +++ b/esphome/components/light/esp_range_view.h @@ -0,0 +1,80 @@ +#pragma once + +#include "esp_color_view.h" +#include "esp_hsv_color.h" + +namespace esphome { +namespace light { + +int32_t interpret_index(int32_t index, int32_t size); + +class AddressableLight; +class ESPRangeIterator; + +/** + * A half-open range of LEDs, inclusive of the begin index and exclusive of the end index, using zero-based numbering. + */ +class ESPRangeView : public ESPColorSettable { + public: + ESPRangeView(AddressableLight *parent, int32_t begin, int32_t end) + : parent_(parent), begin_(begin), end_(end < begin ? begin : end) {} + ESPRangeView(const ESPRangeView &) = default; + + int32_t size() const { return this->end_ - this->begin_; } + ESPColorView operator[](int32_t index) const; + ESPRangeIterator begin(); + ESPRangeIterator end(); + + void set(const Color &color) override; + void set(const ESPHSVColor &color) { this->set(color.to_rgb()); } + void set_red(uint8_t red) override; + void set_green(uint8_t green) override; + void set_blue(uint8_t blue) override; + void set_white(uint8_t white) override; + void set_effect_data(uint8_t effect_data) override; + + void fade_to_white(uint8_t amnt) override; + void fade_to_black(uint8_t amnt) override; + void lighten(uint8_t delta) override; + void darken(uint8_t delta) override; + + ESPRangeView &operator=(const Color &rhs) { + this->set(rhs); + return *this; + } + ESPRangeView &operator=(const ESPColorView &rhs) { + this->set(rhs.get()); + return *this; + } + ESPRangeView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + ESPRangeView &operator=(const ESPRangeView &rhs); + + protected: + friend ESPRangeIterator; + + AddressableLight *parent_; + int32_t begin_; + int32_t end_; +}; + +class ESPRangeIterator { + public: + ESPRangeIterator(const ESPRangeView &range, int32_t i) : range_(range), i_(i) {} + ESPRangeIterator(const ESPRangeIterator &) = default; + ESPRangeIterator operator++() { + this->i_++; + return *this; + } + bool operator!=(const ESPRangeIterator &other) const { return this->i_ != other.i_; } + ESPColorView operator*() const; + + protected: + ESPRangeView range_; + int32_t i_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp new file mode 100644 index 0000000000..9858590850 --- /dev/null +++ b/esphome/components/light/light_call.cpp @@ -0,0 +1,688 @@ +#include "light_call.h" +#include "light_state.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +static const char *const TAG = "light"; + +static const LogString *color_mode_to_human(ColorMode color_mode) { + if (color_mode == ColorMode::UNKNOWN) + return LOG_STR("Unknown"); + if (color_mode == ColorMode::WHITE) + return LOG_STR("White"); + if (color_mode == ColorMode::COLOR_TEMPERATURE) + return LOG_STR("Color temperature"); + if (color_mode == ColorMode::COLD_WARM_WHITE) + return LOG_STR("Cold/warm white"); + if (color_mode == ColorMode::RGB) + return LOG_STR("RGB"); + if (color_mode == ColorMode::RGB_WHITE) + return LOG_STR("RGBW"); + if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) + return LOG_STR("RGB + cold/warm white"); + if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) + return LOG_STR("RGB + color temperature"); + return LOG_STR(""); +} + +void LightCall::perform() { + const char *name = this->parent_->get_name().c_str(); + LightColorValues v = this->validate_(); + + if (this->publish_) { + ESP_LOGD(TAG, "'%s' Setting:", name); + + // Only print color mode when it's being changed + ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); + if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); + } + + // Only print state when it's being changed + bool current_state = this->parent_->remote_values.is_on(); + if (this->state_.value_or(current_state) != current_state) { + ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); + } + + if (this->brightness_.has_value()) { + ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); + } + + if (this->color_brightness_.has_value()) { + ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); + } + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + v.get_blue() * 100.0f); + } + + if (this->white_.has_value()) { + ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); + } + if (this->color_temperature_.has_value()) { + ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + } + + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + v.get_warm_white() * 100.0f); + } + } + + if (this->has_flash_()) { + // FLASH + if (this->publish_) { + ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); + } + + this->parent_->start_flash_(v, *this->flash_length_, this->publish_); + } else if (this->has_transition_()) { + // TRANSITION + if (this->publish_) { + ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); + } + + // Special case: Transition and effect can be set when turning off + if (this->has_effect_()) { + if (this->publish_) { + ESP_LOGD(TAG, " Effect: 'None'"); + } + this->parent_->stop_effect_(); + } + + this->parent_->start_transition_(v, *this->transition_length_, this->publish_); + + } else if (this->has_effect_()) { + // EFFECT + auto effect = this->effect_; + const char *effect_s; + if (effect == 0) + effect_s = "None"; + else + effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); + + if (this->publish_) { + ESP_LOGD(TAG, " Effect: '%s'", effect_s); + } + + this->parent_->start_effect_(*this->effect_); + + // Also set light color values when starting an effect + // For example to turn off the light + this->parent_->set_immediately_(v, true); + } else { + // INSTANT CHANGE + this->parent_->set_immediately_(v, this->publish_); + } + + if (!this->has_transition_()) { + this->parent_->target_state_reached_callback_.call(); + } + if (this->publish_) { + this->parent_->publish_state(); + } + if (this->save_) { + this->parent_->save_remote_values_(); + } +} + +LightColorValues LightCall::validate_() { + auto *name = this->parent_->get_name().c_str(); + auto traits = this->parent_->get_traits(); + + // Color mode check + if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { + ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); + this->color_mode_.reset(); + } + + // Ensure there is always a color mode set + if (!this->color_mode_.has_value()) { + this->color_mode_ = this->compute_color_mode_(); + } + auto color_mode = *this->color_mode_; + + // Transform calls that use non-native parameters for the current mode. + this->transform_parameters_(); + + // Brightness exists check + if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { + ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); + this->brightness_.reset(); + } + + // Transition length possible check + if (this->transition_length_.has_value() && *this->transition_length_ != 0 && + !(color_mode & ColorCapability::BRIGHTNESS)) { + ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); + this->transition_length_.reset(); + } + + // Color brightness exists check + if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); + this->color_brightness_.reset(); + } + + // RGB exists check + if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || + (this->blue_.has_value() && *this->blue_ > 0.0f)) { + if (!(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); + this->red_.reset(); + this->green_.reset(); + this->blue_.reset(); + } + } + + // White value exists check + if (this->white_.has_value() && *this->white_ > 0.0f && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); + this->white_.reset(); + } + + // Color temperature exists check + if (this->color_temperature_.has_value() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); + this->color_temperature_.reset(); + } + + // Cold/warm white value exists check + if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { + if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + this->cold_white_.reset(); + this->warm_white_.reset(); + } + } + +#define VALIDATE_RANGE_(name_, upper_name, min, max) \ + if (name_##_.has_value()) { \ + auto val = *name_##_; \ + if (val < (min) || val > (max)) { \ + ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, LOG_STR_LITERAL(upper_name), val, \ + (min), (max)); \ + name_##_ = clamp(val, (min), (max)); \ + } \ + } +#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) + + // Range checks + VALIDATE_RANGE(brightness, "Brightness") + VALIDATE_RANGE(color_brightness, "Color brightness") + VALIDATE_RANGE(red, "Red") + VALIDATE_RANGE(green, "Green") + VALIDATE_RANGE(blue, "Blue") + VALIDATE_RANGE(white, "White") + VALIDATE_RANGE(cold_white, "Cold white") + VALIDATE_RANGE(warm_white, "Warm white") + VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) + + // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. + bool explicit_turn_off_request = this->state_.has_value() && !*this->state_; + + // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). + if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { + this->state_ = optional(false); + this->brightness_ = optional(1.0f); + } + + // Set color brightness to 100% if currently zero and a color is set. + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) + this->color_brightness_ = optional(1.0f); + } + + // Create color values for the light with this call applied. + auto v = this->parent_->remote_values; + if (this->color_mode_.has_value()) + v.set_color_mode(*this->color_mode_); + if (this->state_.has_value()) + v.set_state(*this->state_); + if (this->brightness_.has_value()) + v.set_brightness(*this->brightness_); + if (this->color_brightness_.has_value()) + v.set_color_brightness(*this->color_brightness_); + if (this->red_.has_value()) + v.set_red(*this->red_); + if (this->green_.has_value()) + v.set_green(*this->green_); + if (this->blue_.has_value()) + v.set_blue(*this->blue_); + if (this->white_.has_value()) + v.set_white(*this->white_); + if (this->color_temperature_.has_value()) + v.set_color_temperature(*this->color_temperature_); + if (this->cold_white_.has_value()) + v.set_cold_white(*this->cold_white_); + if (this->warm_white_.has_value()) + v.set_warm_white(*this->warm_white_); + + v.normalize_color(); + + // Flash length check + if (this->has_flash_() && *this->flash_length_ == 0) { + ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + this->flash_length_.reset(); + } + + // validate transition length/flash length/effect not used at the same time + bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; + + // If effect is already active, remove effect start + if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { + this->effect_.reset(); + } + + // validate effect index + if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { + ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); + this->effect_.reset(); + } + + if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { + ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + this->transition_length_.reset(); + this->flash_length_.reset(); + } + + if (this->has_flash_() && this->has_transition_()) { + ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + this->transition_length_.reset(); + } + + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + supports_transition) { + // nothing specified and light supports transitions, set default transition length + this->transition_length_ = this->parent_->default_transition_length_; + } + + if (this->transition_length_.value_or(0) == 0) { + // 0 transition is interpreted as no transition (instant change) + this->transition_length_.reset(); + } + + if (this->has_transition_() && !supports_transition) { + ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + this->transition_length_.reset(); + } + + // If not a flash and turning the light off, then disable the light + // Do not use light color values directly, so that effects can set 0% brightness + // Reason: When user turns off the light in frontend, the effect should also stop + if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { + if (this->has_effect_()) { + ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + this->effect_.reset(); + } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { + // Auto turn off effect + this->effect_ = 0; + } + } + + // Disable saving for flashes + if (this->has_flash_()) + this->save_ = false; + + return v; +} +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. + 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) && // + !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + 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; + } + } +} +ColorMode LightCall::compute_color_mode_() { + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); + int supported_count = supported_modes.size(); + + // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. + if (supported_count == 0) + return ColorMode::UNKNOWN; + + // In the common case of lights supporting only a single mode, use that one. + if (supported_count == 1) + return *supported_modes.begin(); + + // Don't change if the light is being turned off. + ColorMode current_mode = this->parent_->remote_values.get_color_mode(); + if (this->state_.has_value() && !*this->state_) + return current_mode; + + // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to + // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode + // was used for some reason. + std::set suitable_modes = this->get_suitable_color_modes_(); + + // Don't change if the current mode is suitable. + if (suitable_modes.count(current_mode) > 0) { + ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", + this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); + return current_mode; + } + + // Use the preferred suitable mode. + for (auto mode : suitable_modes) { + if (supported_modes.count(mode) == 0) + continue; + + ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(mode))); + return mode; + } + + // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip + // out whatever we don't support. + auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); + ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", + this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(color_mode))); + return color_mode; +} +std::set LightCall::get_suitable_color_modes_() { + bool has_white = this->white_.has_value() && *this->white_ > 0.0f; + bool has_ct = this->color_temperature_.has_value(); + bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); + bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + +#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) +#define ENTRY(white, ct, cwww, rgb, ...) \ + std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) + + // Flag order: white, color temperature, cwww, rgb + std::array>, 10> lookup_table{ + ENTRY(true, false, false, false, + {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, false, + {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, false, + {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, false, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), + ENTRY(true, false, false, true, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, true, + {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + }; + + auto key = KEY(has_white, has_ct, has_cwww, has_rgb); + for (auto &item : lookup_table) + if (std::get<0>(item) == key) + return std::get<1>(item); + + // This happens if there are conflicting flags given. + return {}; +} + +LightCall &LightCall::set_effect(const std::string &effect) { + if (strcasecmp(effect.c_str(), "none") == 0) { + this->set_effect(0); + return *this; + } + + bool found = false; + for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { + LightEffect *e = this->parent_->effects_[i]; + + if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { + this->set_effect(i + 1); + found = true; + break; + } + } + if (!found) { + ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + } + return *this; +} +LightCall &LightCall::from_light_color_values(const LightColorValues &values) { + this->set_state(values.is_on()); + this->set_brightness_if_supported(values.get_brightness()); + this->set_color_brightness_if_supported(values.get_color_brightness()); + this->set_color_mode_if_supported(values.get_color_mode()); + this->set_red_if_supported(values.get_red()); + this->set_green_if_supported(values.get_green()); + this->set_blue_if_supported(values.get_blue()); + this->set_white_if_supported(values.get_white()); + this->set_color_temperature_if_supported(values.get_color_temperature()); + this->set_cold_white_if_supported(values.get_cold_white()); + this->set_warm_white_if_supported(values.get_warm_white()); + return *this; +} +ColorMode LightCall::get_active_color_mode_() { + return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); +} +LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) + this->set_transition_length(transition_length); + return *this; +} +LightCall &LightCall::set_brightness_if_supported(float brightness) { + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) + this->set_brightness(brightness); + return *this; +} +LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { + if (this->parent_->get_traits().supports_color_mode(color_mode)) + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_brightness_if_supported(float brightness) { + if (this->get_active_color_mode_() & ColorCapability::RGB) + this->set_color_brightness(brightness); + return *this; +} +LightCall &LightCall::set_red_if_supported(float red) { + if (this->get_active_color_mode_() & ColorCapability::RGB) + this->set_red(red); + return *this; +} +LightCall &LightCall::set_green_if_supported(float green) { + if (this->get_active_color_mode_() & ColorCapability::RGB) + this->set_green(green); + return *this; +} +LightCall &LightCall::set_blue_if_supported(float blue) { + if (this->get_active_color_mode_() & ColorCapability::RGB) + this->set_blue(blue); + return *this; +} +LightCall &LightCall::set_white_if_supported(float white) { + if (this->get_active_color_mode_() & ColorCapability::WHITE) + this->set_white(white); + return *this; +} +LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { + if (this->get_active_color_mode_() & ColorCapability::COLOR_TEMPERATURE || + this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_color_temperature(color_temperature); + return *this; +} +LightCall &LightCall::set_cold_white_if_supported(float cold_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_cold_white(cold_white); + return *this; +} +LightCall &LightCall::set_warm_white_if_supported(float warm_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_warm_white(warm_white); + return *this; +} +LightCall &LightCall::set_state(optional state) { + this->state_ = state; + return *this; +} +LightCall &LightCall::set_state(bool state) { + this->state_ = state; + return *this; +} +LightCall &LightCall::set_transition_length(optional transition_length) { + this->transition_length_ = transition_length; + return *this; +} +LightCall &LightCall::set_transition_length(uint32_t transition_length) { + this->transition_length_ = transition_length; + return *this; +} +LightCall &LightCall::set_flash_length(optional flash_length) { + this->flash_length_ = flash_length; + return *this; +} +LightCall &LightCall::set_flash_length(uint32_t flash_length) { + this->flash_length_ = flash_length; + return *this; +} +LightCall &LightCall::set_brightness(optional brightness) { + this->brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_brightness(float brightness) { + this->brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_color_mode(optional color_mode) { + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_mode(ColorMode color_mode) { + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_brightness(optional brightness) { + this->color_brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_color_brightness(float brightness) { + this->color_brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_red(optional red) { + this->red_ = red; + return *this; +} +LightCall &LightCall::set_red(float red) { + this->red_ = red; + return *this; +} +LightCall &LightCall::set_green(optional green) { + this->green_ = green; + return *this; +} +LightCall &LightCall::set_green(float green) { + this->green_ = green; + return *this; +} +LightCall &LightCall::set_blue(optional blue) { + this->blue_ = blue; + return *this; +} +LightCall &LightCall::set_blue(float blue) { + this->blue_ = blue; + return *this; +} +LightCall &LightCall::set_white(optional white) { + this->white_ = white; + return *this; +} +LightCall &LightCall::set_white(float white) { + this->white_ = white; + return *this; +} +LightCall &LightCall::set_color_temperature(optional color_temperature) { + this->color_temperature_ = color_temperature; + return *this; +} +LightCall &LightCall::set_color_temperature(float color_temperature) { + this->color_temperature_ = color_temperature; + return *this; +} +LightCall &LightCall::set_cold_white(optional cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_cold_white(float cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_warm_white(optional warm_white) { + this->warm_white_ = warm_white; + return *this; +} +LightCall &LightCall::set_warm_white(float warm_white) { + this->warm_white_ = warm_white; + return *this; +} +LightCall &LightCall::set_effect(optional effect) { + if (effect.has_value()) + this->set_effect(*effect); + return *this; +} +LightCall &LightCall::set_effect(uint32_t effect_number) { + this->effect_ = effect_number; + return *this; +} +LightCall &LightCall::set_effect(optional effect_number) { + this->effect_ = effect_number; + return *this; +} +LightCall &LightCall::set_publish(bool publish) { + this->publish_ = publish; + return *this; +} +LightCall &LightCall::set_save(bool save) { + this->save_ = save; + return *this; +} +LightCall &LightCall::set_rgb(float red, float green, float blue) { + this->set_red(red); + this->set_green(green); + this->set_blue(blue); + return *this; +} +LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) { + this->set_rgb(red, green, blue); + this->set_white(white); + return *this; +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h new file mode 100644 index 0000000000..bca2ac7b07 --- /dev/null +++ b/esphome/components/light/light_call.h @@ -0,0 +1,197 @@ +#pragma once + +#include "esphome/core/optional.h" +#include "light_color_values.h" +#include + +namespace esphome { +namespace light { + +class LightState; + +/** This class represents a requested change in a light state. + */ +class LightCall { + public: + explicit LightCall(LightState *parent) : parent_(parent) {} + + /// Set the binary ON/OFF state of the light. + LightCall &set_state(optional state); + /// Set the binary ON/OFF state of the light. + LightCall &set_state(bool state); + /** Set the transition length of this call in milliseconds. + * + * This argument is ignored for starting flashes and effects. + * + * Defaults to the default transition length defined in the light configuration. + */ + LightCall &set_transition_length(optional transition_length); + /** Set the transition length of this call in milliseconds. + * + * This argument is ignored for starting flashes and effects. + * + * Defaults to the default transition length defined in the light configuration. + */ + LightCall &set_transition_length(uint32_t transition_length); + /// Set the transition length property if the light supports transitions. + LightCall &set_transition_length_if_supported(uint32_t transition_length); + /// Start and set the flash length of this call in milliseconds. + LightCall &set_flash_length(optional flash_length); + /// Start and set the flash length of this call in milliseconds. + LightCall &set_flash_length(uint32_t flash_length); + /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) + LightCall &set_brightness(optional brightness); + /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) + LightCall &set_brightness(float brightness); + /// Set the brightness property if the light supports brightness. + LightCall &set_brightness_if_supported(float brightness); + + /// Set the color mode of the light. + LightCall &set_color_mode(optional color_mode); + /// Set the color mode of the light. + LightCall &set_color_mode(ColorMode color_mode); + /// Set the color mode of the light, if this mode is supported. + LightCall &set_color_mode_if_supported(ColorMode color_mode); + + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(optional brightness); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(float brightness); + /// Set the color brightness property if the light supports RGBW. + LightCall &set_color_brightness_if_supported(float brightness); + /** Set the red RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_red(optional red); + /** Set the red RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_red(float red); + /// Set the red property if the light supports RGB. + LightCall &set_red_if_supported(float red); + /** Set the green RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_green(optional green); + /** Set the green RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_green(float green); + /// Set the green property if the light supports RGB. + LightCall &set_green_if_supported(float green); + /** Set the blue RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_blue(optional blue); + /** Set the blue RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_blue(float blue); + /// Set the blue property if the light supports RGB. + LightCall &set_blue_if_supported(float blue); + /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. + LightCall &set_white(optional white); + /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. + LightCall &set_white(float white); + /// Set the white property if the light supports RGB. + LightCall &set_white_if_supported(float white); + /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. + LightCall &set_color_temperature(optional color_temperature); + /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. + LightCall &set_color_temperature(float color_temperature); + /// Set the color_temperature property if the light supports color temperature. + LightCall &set_color_temperature_if_supported(float color_temperature); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(optional cold_white); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(float cold_white); + /// Set the cold white property if the light supports cold white output. + LightCall &set_cold_white_if_supported(float cold_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(optional warm_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(float warm_white); + /// Set the warm white property if the light supports cold white output. + LightCall &set_warm_white_if_supported(float warm_white); + /// Set the effect of the light by its name. + LightCall &set_effect(optional effect); + /// Set the effect of the light by its name. + LightCall &set_effect(const std::string &effect); + /// Set the effect of the light by its internal index number (only for internal use). + LightCall &set_effect(uint32_t effect_number); + LightCall &set_effect(optional effect_number); + /// Set whether this light call should trigger a publish state. + LightCall &set_publish(bool publish); + /// Set whether this light call should trigger a save state to recover them at startup.. + LightCall &set_save(bool save); + + /** Set the RGB color of the light by RGB values. + * + * Please note that this only changes the color of the light, not the brightness. + * + * @param red The red color value from 0.0 to 1.0. + * @param green The green color value from 0.0 to 1.0. + * @param blue The blue color value from 0.0 to 1.0. + * @return The light call for chaining setters. + */ + LightCall &set_rgb(float red, float green, float blue); + /** Set the RGBW color of the light by RGB values. + * + * Please note that this only changes the color of the light, not the brightness. + * + * @param red The red color value from 0.0 to 1.0. + * @param green The green color value from 0.0 to 1.0. + * @param blue The blue color value from 0.0 to 1.0. + * @param white The white color value from 0.0 to 1.0. + * @return The light call for chaining setters. + */ + LightCall &set_rgbw(float red, float green, float blue, float white); + LightCall &from_light_color_values(const LightColorValues &values); + + void perform(); + + protected: + /// Get the currently targeted, or active if none set, color mode. + ColorMode get_active_color_mode_(); + + /// Validate all properties and return the target light color values. + LightColorValues validate_(); + + //// Compute the color mode that should be used for this call. + ColorMode compute_color_mode_(); + /// Get potential color modes for this light call. + std::set get_suitable_color_modes_(); + /// Some color modes also can be set using non-native parameters, transform those calls. + void transform_parameters_(); + + bool has_transition_() { return this->transition_length_.has_value(); } + bool has_flash_() { return this->flash_length_.has_value(); } + bool has_effect_() { return this->effect_.has_value(); } + + LightState *parent_; + optional state_; + optional transition_length_; + optional flash_length_; + optional color_mode_; + optional brightness_; + optional color_brightness_; + optional red_; + optional green_; + optional blue_; + optional white_; + optional color_temperature_; + optional cold_white_; + optional warm_white_; + optional effect_; + bool publish_{true}; + bool save_{true}; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index cdd05ae7b7..ffbe378ee3 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -1,86 +1,76 @@ #pragma once #include "esphome/core/helpers.h" -#include "esphome/core/defines.h" -#include "light_traits.h" - -#ifdef USE_JSON -#include "esphome/components/json/json_util.h" -#endif +#include "color_mode.h" +#include namespace esphome { namespace light { +inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } + /** This class represents the color state for a light object. * - * All values in this class are represented using floats in the range from 0.0 (off) to 1.0 (on). - * Not all values have to be populated though, for example a simple monochromatic light only needs - * to access the state and brightness attributes. + * The representation of the color state is dependent on the active color mode. A color mode consists of multiple + * color capabilities, and each color capability has its own representation in this class. The fields available are as + * follows: * - * Please note all float values are automatically clamped. + * Always: + * - color_mode: The currently active color mode. * - * state - Whether the light should be on/off. Represented as a float for transitions. - * brightness - The brightness of the light. - * red, green, blue - RGB values. - * white - The white value for RGBW lights. - * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) + * For ON_OFF capability: + * - state: Whether the light should be on/off. Represented as a float for transitions. + * + * For BRIGHTNESS capability: + * - brightness: The master brightness of the light, should be applied to all channels. + * + * For RGB capability: + * - color_brightness: The brightness of the color channels of the light. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of them is always 1.0. + * + * For WHITE capability: + * - white: The brightness of the white channel of the light. + * + * For COLOR_TEMPERATURE capability: + * - color_temperature: The color temperature of the white channel in mireds. Note that it is not clamped to the valid + * range as set in the traits, so the output needs to do this. + * + * For COLD_WARM_WHITE capability: + * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * + * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are + * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values + * and must not be accessed by the light output. */ class LightColorValues { public: - /// Construct the LightColorValues with all attributes enabled, but state set to 0.0 + /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : state_(0.0f), + : color_mode_(ColorMode::UNKNOWN), + state_(0.0f), brightness_(1.0f), + color_brightness_(1.0f), red_(1.0f), green_(1.0f), blue_(1.0f), white_(1.0f), - color_temperature_{1.0f} {} + color_temperature_{0.0f}, + cold_white_{1.0f}, + warm_white_{1.0f} {} - LightColorValues(float state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) { + LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, + float blue, float white, float color_temperature, float cold_white, float warm_white) { + this->set_color_mode(color_mode); this->set_state(state); this->set_brightness(brightness); + this->set_color_brightness(color_brightness); this->set_red(red); this->set_green(green); this->set_blue(blue); this->set_white(white); this->set_color_temperature(color_temperature); - } - - LightColorValues(bool state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) - : LightColorValues(state ? 1.0f : 0.0f, brightness, red, green, blue, white, color_temperature) {} - - /// Create light color values from a binary true/false state. - static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } - - /// Create light color values from a monochromatic brightness state. - static LightColorValues from_monochromatic(float brightness) { - if (brightness == 0.0f) - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - else - return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f}; - } - - /// Create light color values from an RGB state. - static LightColorValues from_rgb(float r, float g, float b) { - float brightness = std::max(r, std::max(g, b)); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, 1.0f}; - } - } - - /// Create light color values from an RGBW state. - static LightColorValues from_rgbw(float r, float g, float b, float w) { - float brightness = std::max(r, std::max(g, std::max(b, w))); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, w / brightness}; - } + this->set_cold_white(cold_white); + this->set_warm_white(warm_white); } /** Linearly interpolate between the values in start to the values in end. @@ -95,57 +85,32 @@ class LightColorValues { */ static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; + v.set_color_mode(end.color_mode_); v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); + v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); + v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); + v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); return v; } -#ifdef USE_JSON - /** Dump this color into a JsonObject. Only dumps values if the corresponding traits are marked supported by traits. - * - * @param root The json root object. - * @param traits The traits object used for determining whether to include certain attributes. - */ - void dump_json(JsonObject &root, const LightTraits &traits) const { - root["state"] = (this->get_state() != 0.0f) ? "ON" : "OFF"; - if (traits.get_supports_brightness()) - root["brightness"] = uint8_t(this->get_brightness() * 255); - if (traits.get_supports_rgb()) { - JsonObject &color = root.createNestedObject("color"); - color["r"] = uint8_t(this->get_red() * 255); - color["g"] = uint8_t(this->get_green() * 255); - color["b"] = uint8_t(this->get_blue() * 255); - } - if (traits.get_supports_rgb_white_value()) - root["white_value"] = uint8_t(this->get_white() * 255); - if (traits.get_supports_color_temperature()) - root["color_temp"] = uint32_t(this->get_color_temperature()); - } -#endif - /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. - * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8 + * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8. + * + * Note that this does NOT retain the brightness information from the color attributes. * * @param traits Used for determining which attributes to consider. */ - void normalize_color(const LightTraits &traits) { - if (traits.get_supports_rgb()) { + void normalize_color() { + if (this->color_mode_ & ColorCapability::RGB) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); - if (traits.get_supports_rgb_white_value()) { - max_value = fmaxf(max_value, this->get_white()); - if (max_value == 0.0f) { - this->set_white(1.0f); - } else { - this->set_white(this->get_white() / max_value); - } - } if (max_value == 0.0f) { this->set_red(1.0f); this->set_green(1.0f); @@ -156,20 +121,11 @@ class LightColorValues { this->set_blue(this->get_blue() / max_value); } } - - if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { - if (traits.get_supports_rgb_white_value()) { - // 0% brightness for RGBW[W] means no RGB channel, but white channel on. - // do nothing - } else { - // 0% brightness means off - this->set_state(false); - // reset brightness to 100% - this->set_brightness(1.0f); - } - } } + // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters + // are always used or necessary. Methods will be deprecated later. + /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } @@ -180,63 +136,92 @@ class LightColorValues { /// Convert these light color values to an RGB representation and write them to red, green, blue. void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { - float brightness = this->state_ * this->brightness_; - if (color_interlock) { - brightness = brightness * (1.0f - this->white_); + if (this->color_mode_ & ColorCapability::RGB) { + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); + } else { + *red = *green = *blue = 0; } - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - } - - /// Convert these light color values to an RGBWW representation with the given parameters. - void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, - bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + this->as_rgb(red, green, blue, gamma); + if (this->color_mode_ & ColorCapability::WHITE) { + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + } else { + *white = 0; } } + /// Convert these light color values to an RGBWW representation with the given parameters. + void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, + bool constant_brightness = false) const { + this->as_rgb(red, green, blue, gamma); + this->as_cwww(cold_white, warm_white, gamma, constant_brightness); + } + + /// Convert these light color values to an RGB+CT+BR representation with the given parameters. + void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, + float *color_temperature, float *white_brightness, float gamma = 0) const { + this->as_rgb(red, green, blue, gamma); + this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma); + } + /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, - float gamma = 0, bool constant_brightness = false) const { - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { + const float cw_level = gamma_correct(this->cold_white_, gamma); + const float ww_level = gamma_correct(this->warm_white_, gamma); + const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + if (!constant_brightness) { + *cold_white = white_level * cw_level; + *warm_white = white_level * ww_level; + } else { + // Just multiplying by cw_level / (cw_level + ww_level) would divide out the brightness information from the + // cold_white and warm_white settings (i.e. cw=0.8, ww=0.4 would be identical to cw=0.4, ww=0.2), which breaks + // transitions. Use the highest value as the brightness for the white channels (the alternative, using cw+ww/2, + // reduces to cw/2 and ww/2, which would still limit brightness to 100% of a single channel, but isn't very + // useful in all other aspects -- that behaviour can also be achieved by limiting the output power). + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; + } + } else { + *cold_white = *warm_white = 0; + } + } + + /// Convert these light color values to a CT+BR representation with the given parameters. + void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness, + float gamma = 0) const { + const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1; + if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) { + *color_temperature = + (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); + *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); + } else { // Probably wont get here but put this here anyway. + *white_brightness = 0; } } /// Compare this LightColorValues to rhs, return true if and only if all attributes match. bool operator==(const LightColorValues &rhs) const { - return state_ == rhs.state_ && brightness_ == rhs.brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && - blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_; + return color_mode_ == rhs.color_mode_ && state_ == rhs.state_ && brightness_ == rhs.brightness_ && + color_brightness_ == rhs.color_brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && + blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_ && + cold_white_ == rhs.cold_white_ && warm_white_ == rhs.warm_white_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } + /// Get the color mode of these light color values. + ColorMode get_color_mode() const { return this->color_mode_; } + /// Set the color mode of these light color values. + void set_color_mode(ColorMode color_mode) { this->color_mode_ = color_mode; } + /// Get the state of these light color values. In range from 0.0 (off) to 1.0 (on) float get_state() const { return this->state_; } /// Get the binary true/false state of these light color values. @@ -251,6 +236,11 @@ class LightColorValues { /// Set the brightness property of these light color values. In range 0.0 to 1.0 void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the color brightness property of these light color values. In range 0.0 to 1.0 + float get_color_brightness() const { return this->color_brightness_; } + /// Set the color brightness property of these light color values. In range 0.0 to 1.0 + void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the red property of these light color values. In range 0.0 to 1.0 float get_red() const { return this->red_; } /// Set the red property of these light color values. In range 0.0 to 1.0 @@ -274,18 +264,30 @@ class LightColorValues { /// Get the color temperature property of these light color values in mired. float get_color_temperature() const { return this->color_temperature_; } /// Set the color temperature property of these light color values in mired. - void set_color_temperature(float color_temperature) { - this->color_temperature_ = std::max(0.000001f, color_temperature); - } + void set_color_temperature(float color_temperature) { this->color_temperature_ = 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. + void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); } + + /// Get the warm white property of these light color values. In range 0.0 to 1.0. + float get_warm_white() const { return this->warm_white_; } + /// Set the warm white property of these light color values. In range 0.0 to 1.0. + void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: + ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; + float color_brightness_; float red_; float green_; float blue_; float white_; float color_temperature_; ///< Color Temperature in Mired + float cold_white_; + float warm_white_; }; } // namespace light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index f9903397b4..8da51fe8b3 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -3,8 +3,6 @@ #include #include "esphome/core/component.h" -#include "light_color_values.h" -#include "light_state.h" namespace esphome { namespace light { diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp new file mode 100644 index 0000000000..2e07d91046 --- /dev/null +++ b/esphome/components/light/light_json_schema.cpp @@ -0,0 +1,165 @@ +#include "light_json_schema.h" +#include "light_output.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema + +void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { + if (state.supports_effects()) + root["effect"] = state.get_effect_name(); + + auto values = state.remote_values; + auto traits = state.get_output()->get_traits(); + + switch (values.get_color_mode()) { + case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it + break; + case ColorMode::ON_OFF: + root["color_mode"] = "onoff"; + break; + case ColorMode::BRIGHTNESS: + root["color_mode"] = "brightness"; + break; + case ColorMode::WHITE: // not supported by HA in MQTT + root["color_mode"] = "white"; + break; + case ColorMode::COLOR_TEMPERATURE: + root["color_mode"] = "color_temp"; + break; + case ColorMode::COLD_WARM_WHITE: // not supported by HA + root["color_mode"] = "cwww"; + break; + case ColorMode::RGB: + root["color_mode"] = "rgb"; + break; + case ColorMode::RGB_WHITE: + root["color_mode"] = "rgbw"; + break; + case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA + root["color_mode"] = "rgbct"; + break; + case ColorMode::RGB_COLD_WARM_WHITE: + root["color_mode"] = "rgbww"; + break; + } + + if (values.get_color_mode() & ColorCapability::ON_OFF) + root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; + if (values.get_color_mode() & ColorCapability::BRIGHTNESS) + root["brightness"] = uint8_t(values.get_brightness() * 255); + + JsonObject &color = root.createNestedObject("color"); + if (values.get_color_mode() & ColorCapability::RGB) { + color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); + color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); + color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255); + } + if (values.get_color_mode() & ColorCapability::WHITE) { + color["w"] = uint8_t(values.get_white() * 255); + root["white_value"] = uint8_t(values.get_white() * 255); // legacy API + } + if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) { + // this one isn't under the color subkey for some reason + root["color_temp"] = uint32_t(values.get_color_temperature()); + } + if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) { + color["c"] = uint8_t(values.get_cold_white() * 255); + color["w"] = uint8_t(values.get_warm_white() * 255); + } +} + +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { + if (root.containsKey("state")) { + auto val = parse_on_off(root["state"]); + switch (val) { + case PARSE_ON: + call.set_state(true); + break; + case PARSE_OFF: + call.set_state(false); + break; + case PARSE_TOGGLE: + call.set_state(!state.remote_values.is_on()); + break; + case PARSE_NONE: + break; + } + } + + if (root.containsKey("brightness")) { + call.set_brightness(float(root["brightness"]) / 255.0f); + } + + if (root.containsKey("color")) { + JsonObject &color = root["color"]; + // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. + float max_rgb = 0.0f; + if (color.containsKey("r")) { + float r = float(color["r"]) / 255.0f; + max_rgb = fmaxf(max_rgb, r); + call.set_red(r); + } + if (color.containsKey("g")) { + float g = float(color["g"]) / 255.0f; + max_rgb = fmaxf(max_rgb, g); + call.set_green(g); + } + if (color.containsKey("b")) { + float b = float(color["b"]) / 255.0f; + max_rgb = fmaxf(max_rgb, b); + call.set_blue(b); + } + if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + call.set_color_brightness(max_rgb); + } + + if (color.containsKey("c")) { + call.set_cold_white(float(color["c"]) / 255.0f); + } + if (color.containsKey("w")) { + // the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm + // white channel in RGBWW. + if (color.containsKey("c")) { + call.set_warm_white(float(color["w"]) / 255.0f); + } else { + call.set_white(float(color["w"]) / 255.0f); + } + } + } + + if (root.containsKey("white_value")) { // legacy API + call.set_white(float(root["white_value"]) / 255.0f); + } + + if (root.containsKey("color_temp")) { + call.set_color_temperature(float(root["color_temp"])); + } +} + +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { + LightJSONSchema::parse_color_json(state, call, root); + + if (root.containsKey("flash")) { + auto length = uint32_t(float(root["flash"]) * 1000); + call.set_flash_length(length); + } + + if (root.containsKey("transition")) { + auto length = uint32_t(float(root["transition"]) * 1000); + call.set_transition_length(length); + } + + if (root.containsKey("effect")) { + const char *effect = root["effect"]; + call.set_effect(effect); + } +} + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h new file mode 100644 index 0000000000..09a372f11c --- /dev/null +++ b/esphome/components/light/light_json_schema.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_JSON + +#include "esphome/components/json/json_util.h" +#include "light_call.h" +#include "light_state.h" + +namespace esphome { +namespace light { + +class LightJSONSchema { + public: + /// Dump the state of a light as JSON. + static void dump_json(LightState &state, JsonObject &root); + /// Parse the JSON state of a light to a LightCall. + static void parse_json(LightState &state, LightCall &call, JsonObject &root); + + protected: + static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); +}; + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp new file mode 100644 index 0000000000..e805a0b694 --- /dev/null +++ b/esphome/components/light/light_output.cpp @@ -0,0 +1,12 @@ +#include "light_output.h" +#include "transformers.h" + +namespace esphome { +namespace light { + +std::unique_ptr LightOutput::create_default_transition() { + return make_unique(); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 9e47092b0f..73ba0371cd 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -3,20 +3,29 @@ #include "esphome/core/component.h" #include "light_traits.h" #include "light_state.h" +#include "light_transformer.h" namespace esphome { namespace light { -class LightState; - /// Interface to write LightStates to hardware. class LightOutput { public: /// Return the LightTraits of this LightOutput. virtual LightTraits get_traits() = 0; + /// Return the default transformer used for transitions. + virtual std::unique_ptr create_default_transition(); + virtual void setup_state(LightState *state) {} + /// Called on every update of the current values of the associated LightState, + /// can optionally be used to do processing of this change. + virtual void update_state(LightState *state) {} + + /// Called from loop() every time the light state has changed, and should + /// should write the new state to hardware. Every call to write_state() is + /// preceded by (at least) one call to update_state(). virtual void write_state(LightState *state) = 0; }; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 177dbb8d4e..5f16585c36 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,95 +1,34 @@ +#include "esphome/core/log.h" #include "light_state.h" #include "light_output.h" -#include "esphome/core/log.h" +#include "transformers.h" namespace esphome { namespace light { static const char *const TAG = "light"; -void LightState::start_transition_(const LightColorValues &target, uint32_t length) { - this->transformer_ = make_unique(millis(), length, this->current_values, target); - this->remote_values = this->transformer_->get_remote_values(); -} +LightState::LightState(const std::string &name, LightOutput *output) : EntityBase(name), output_(output) {} +LightState::LightState(LightOutput *output) : output_(output) {} -void LightState::start_flash_(const LightColorValues &target, uint32_t length) { - LightColorValues end_colors = this->current_values; - // If starting a flash if one is already happening, set end values to end values of current flash - // Hacky but works - if (this->transformer_ != nullptr) - end_colors = this->transformer_->get_end_values(); - this->transformer_ = make_unique(millis(), length, end_colors, target); - this->remote_values = this->transformer_->get_remote_values(); -} - -LightState::LightState(const std::string &name, LightOutput *output) : Nameable(name), output_(output) {} - -void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { - this->transformer_ = nullptr; - this->current_values = target; - if (set_remote_values) { - this->remote_values = target; - } - this->next_write_ = true; -} - -LightColorValues LightState::get_current_values() { return this->current_values; } - -void LightState::publish_state() { - this->remote_values_callback_.call(); - this->next_write_ = true; -} - -LightColorValues LightState::get_remote_values() { return this->remote_values; } - -std::string LightState::get_effect_name() { - if (this->active_effect_index_ > 0) - return this->effects_[this->active_effect_index_ - 1]->get_name(); - else - return "None"; -} - -void LightState::start_effect_(uint32_t effect_index) { - this->stop_effect_(); - if (effect_index == 0) - return; - - this->active_effect_index_ = effect_index; - auto *effect = this->get_active_effect_(); - effect->start_internal(); -} - -bool LightState::supports_effects() { return !this->effects_.empty(); } -void LightState::set_transformer_(std::unique_ptr transformer) { - this->transformer_ = std::move(transformer); -} -void LightState::stop_effect_() { - auto *effect = this->get_active_effect_(); - if (effect != nullptr) { - effect->stop(); - } - this->active_effect_index_ = 0; -} - -void LightState::set_default_transition_length(uint32_t default_transition_length) { - this->default_transition_length_ = default_transition_length; -} -#ifdef USE_JSON -void LightState::dump_json(JsonObject &root) { - if (this->supports_effects()) - root["effect"] = this->get_effect_name(); - this->remote_values.dump_json(root, this->output_->get_traits()); -} -#endif +LightTraits LightState::get_traits() { return this->output_->get_traits(); } +LightCall LightState::turn_on() { return this->make_call().set_state(true); } +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}; }; @@ -101,6 +40,13 @@ void LightState::setup() { effect->init_internal(this); } + // When supported color temperature range is known, initialize color temperature setting within bounds. + float min_mireds = this->get_traits().get_min_mireds(); + if (min_mireds > 0) { + this->remote_values.set_color_temperature(min_mireds); + this->current_values.set_color_temperature(min_mireds); + } + auto call = this->make_call(); LightStateRTCState recovered{}; switch (this->restore_mode_) { @@ -108,7 +54,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = false; @@ -130,13 +76,17 @@ void LightState::setup() { break; } + call.set_color_mode_if_supported(recovered.color_mode); call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); + call.set_color_brightness_if_supported(recovered.color_brightness); call.set_red_if_supported(recovered.red); call.set_green_if_supported(recovered.green); call.set_blue_if_supported(recovered.blue); call.set_white_if_supported(recovered.white); call.set_color_temperature_if_supported(recovered.color_temp); + call.set_cold_white_if_supported(recovered.cold_white); + call.set_warm_white_if_supported(recovered.warm_white); if (recovered.effect != 0) { call.set_effect(recovered.effect); } else { @@ -144,6 +94,17 @@ void LightState::setup() { } call.perform(); } +void LightState::dump_config() { + ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); + if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) { + ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); + ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); + } + if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { + ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); + ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); + } +} void LightState::loop() { // Apply effect (if any) auto *effect = this->get_active_effect_(); @@ -153,601 +114,43 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { + auto values = this->transformer_->apply(); + if (values.has_value()) { + this->current_values = *values; + this->output_->update_state(this); + this->next_write_ = true; + } + if (this->transformer_->is_finished()) { - this->remote_values = this->current_values = this->transformer_->get_end_values(); - this->target_state_reached_callback_.call(); - if (this->transformer_->publish_at_end()) - this->publish_state(); + // if the transition has written directly to the output, current_values is outdated, so update it + this->current_values = this->transformer_->get_target_values(); + + this->transformer_->stop(); this->transformer_ = nullptr; - } else { - this->current_values = this->transformer_->get_values(); - this->remote_values = this->transformer_->get_remote_values(); + this->target_state_reached_callback_.call(); } - this->next_write_ = true; } + // Write state to the light if (this->next_write_) { - this->output_->write_state(this); this->next_write_ = false; + this->output_->write_state(this); } } -LightTraits LightState::get_traits() { return this->output_->get_traits(); } -const std::vector &LightState::get_effects() const { return this->effects_; } -void LightState::add_effects(const std::vector &effects) { - this->effects_.reserve(this->effects_.size() + effects.size()); - for (auto *effect : effects) { - this->effects_.push_back(effect); - } -} -LightCall LightState::turn_on() { return this->make_call().set_state(true); } -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); } -uint32_t LightState::hash_base() { return 1114400283; } -void LightState::dump_config() { - ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().get_supports_brightness()) { - ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); - ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); - } - if (this->get_traits().get_supports_color_temperature()) { - ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); - ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); - } -} -#ifdef USE_MQTT_LIGHT -MQTTJSONLightComponent *LightState::get_mqtt() const { return this->mqtt_; } -void LightState::set_mqtt(MQTTJSONLightComponent *mqtt) { this->mqtt_ = mqtt; } -#endif - -#ifdef USE_JSON -LightCall &LightCall::parse_color_json(JsonObject &root) { - if (root.containsKey("state")) { - auto val = parse_on_off(root["state"]); - switch (val) { - case PARSE_ON: - this->set_state(true); - break; - case PARSE_OFF: - this->set_state(false); - break; - case PARSE_TOGGLE: - this->set_state(!this->parent_->remote_values.is_on()); - break; - case PARSE_NONE: - break; - } - } - - if (root.containsKey("brightness")) { - this->set_brightness(float(root["brightness"]) / 255.0f); - } - - if (root.containsKey("color")) { - JsonObject &color = root["color"]; - if (color.containsKey("r")) { - this->set_red(float(color["r"]) / 255.0f); - } - if (color.containsKey("g")) { - this->set_green(float(color["g"]) / 255.0f); - } - if (color.containsKey("b")) { - this->set_blue(float(color["b"]) / 255.0f); - } - } - - if (root.containsKey("white_value")) { - this->set_white(float(root["white_value"]) / 255.0f); - } - - if (root.containsKey("color_temp")) { - this->set_color_temperature(float(root["color_temp"])); - } - - return *this; -} -LightCall &LightCall::parse_json(JsonObject &root) { - this->parse_color_json(root); - - if (root.containsKey("flash")) { - auto length = uint32_t(float(root["flash"]) * 1000); - this->set_flash_length(length); - } - - if (root.containsKey("transition")) { - auto length = uint32_t(float(root["transition"]) * 1000); - this->set_transition_length(length); - } - - if (root.containsKey("effect")) { - const char *effect = root["effect"]; - this->set_effect(effect); - } - - return *this; -} -#endif - -void LightCall::perform() { - // use remote values for fallback - const char *name = this->parent_->get_name().c_str(); - if (this->publish_) { - ESP_LOGD(TAG, "'%s' Setting:", name); - } - - LightColorValues v = this->validate_(); - - if (this->publish_) { - // Only print state when it's being changed - bool current_state = this->parent_->remote_values.is_on(); - if (this->state_.value_or(current_state) != current_state) { - ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); - } - - if (this->brightness_.has_value()) { - ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); - } - - if (this->color_temperature_.has_value()) { - ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); - } - - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, - v.get_blue() * 100.0f); - } - if (this->white_.has_value()) { - ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); - } - } - - if (this->has_flash_()) { - // FLASH - if (this->publish_) { - ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); - } - - this->parent_->start_flash_(v, *this->flash_length_); - } else if (this->has_transition_()) { - // TRANSITION - if (this->publish_) { - ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); - } - - // Special case: Transition and effect can be set when turning off - if (this->has_effect_()) { - if (this->publish_) { - ESP_LOGD(TAG, " Effect: 'None'"); - } - this->parent_->stop_effect_(); - } - - this->parent_->start_transition_(v, *this->transition_length_); - - } else if (this->has_effect_()) { - // EFFECT - auto effect = this->effect_; - const char *effect_s; - if (effect == 0) - effect_s = "None"; - else - effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); - - if (this->publish_) { - ESP_LOGD(TAG, " Effect: '%s'", effect_s); - } - - this->parent_->start_effect_(*this->effect_); - - // Also set light color values when starting an effect - // For example to turn off the light - this->parent_->set_immediately_(v, true); - } else { - // INSTANT CHANGE - this->parent_->set_immediately_(v, this->publish_); - } - - if (!this->has_transition_()) { - this->parent_->target_state_reached_callback_.call(); - } - if (this->publish_) { - this->parent_->publish_state(); - } - - if (this->save_) { - LightStateRTCState saved; - saved.state = v.is_on(); - saved.brightness = v.get_brightness(); - saved.red = v.get_red(); - saved.green = v.get_green(); - saved.blue = v.get_blue(); - saved.white = v.get_white(); - saved.color_temp = v.get_color_temperature(); - saved.effect = this->parent_->active_effect_index_; - this->parent_->rtc_.save(&saved); - } -} - -LightColorValues LightCall::validate_() { - // use remote values for fallback - auto *name = this->parent_->get_name().c_str(); - auto traits = this->parent_->get_traits(); - - // Brightness exists check - if (this->brightness_.has_value() && !traits.get_supports_brightness()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); - this->brightness_.reset(); - } - - // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); - this->transition_length_.reset(); - } - - // RGB exists check - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); - this->red_.reset(); - this->green_.reset(); - this->blue_.reset(); - } - } - - // White value exists check - if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); - this->white_.reset(); - } - - // Color temperature exists check - if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); - this->color_temperature_.reset(); - } - - // If white channel is specified, set RGB to white color (when interlock is enabled) - if (this->white_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - } - // make white values binary aka 0.0f or 1.0f... this allows brightness to do its job - if (*this->white_ > 0.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color channel is specified, set white channel to 100% for white, otherwise 0% (when interlock is enabled) - else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color temperature is specified, change to white light - else if (this->color_temperature_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - - // if setting color temperature from color (i.e. switching to white light), set White to 100% - auto cv = this->parent_->remote_values; - bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; - if (traits.get_supports_color_interlock() || was_color) { - this->white_ = optional(1.0f); - } - } - -#define VALIDATE_RANGE_(name_, upper_name) \ - if (name_##_.has_value()) { \ - auto val = *name_##_; \ - if (val < 0.0f || val > 1.0f) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [0.0 - 1.0]!", name, upper_name, val); \ - name_##_ = clamp(val, 0.0f, 1.0f); \ - } \ - } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name) - - // Range checks - VALIDATE_RANGE(brightness, "Brightness") - VALIDATE_RANGE(red, "Red") - VALIDATE_RANGE(green, "Green") - VALIDATE_RANGE(blue, "Blue") - VALIDATE_RANGE(white, "White") - - auto v = this->parent_->remote_values; - if (this->state_.has_value()) - v.set_state(*this->state_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); - - if (this->red_.has_value()) - v.set_red(*this->red_); - if (this->green_.has_value()) - v.set_green(*this->green_); - if (this->blue_.has_value()) - v.set_blue(*this->blue_); - if (this->white_.has_value()) - v.set_white(*this->white_); - - if (this->color_temperature_.has_value()) - v.set_color_temperature(*this->color_temperature_); - - v.normalize_color(traits); - - // Flash length check - if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); - this->flash_length_.reset(); - } - - // validate transition length/flash length/effect not used at the same time - bool supports_transition = traits.get_supports_brightness(); - - // If effect is already active, remove effect start - if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { - this->effect_.reset(); - } - - // validate effect index - if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); - this->effect_.reset(); - } - - if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); - this->transition_length_.reset(); - this->flash_length_.reset(); - } - - if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); - this->transition_length_.reset(); - } - - if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && - supports_transition) { - // nothing specified and light supports transitions, set default transition length - this->transition_length_ = this->parent_->default_transition_length_; - } - - if (this->transition_length_.value_or(0) == 0) { - // 0 transition is interpreted as no transition (instant change) - this->transition_length_.reset(); - } - - if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); - this->transition_length_.reset(); - } - - // If not a flash and turning the light off, then disable the light - // Do not use light color values directly, so that effects can set 0% brightness - // Reason: When user turns off the light in frontend, the effect should also stop - if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { - if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); - this->effect_.reset(); - } else if (this->parent_->active_effect_index_ != 0) { - // Auto turn off effect - this->effect_ = 0; - } - } - - // Disable saving for flashes - if (this->has_flash_()) - this->save_ = false; - - return v; -} -LightCall &LightCall::set_effect(const std::string &effect) { - if (strcasecmp(effect.c_str(), "none") == 0) { - this->set_effect(0); - return *this; - } - - bool found = false; - for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { - LightEffect *e = this->parent_->effects_[i]; - - if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { - this->set_effect(i + 1); - found = true; - break; - } - } - if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); - } - return *this; -} -LightCall &LightCall::from_light_color_values(const LightColorValues &values) { - this->set_state(values.is_on()); - this->set_brightness_if_supported(values.get_brightness()); - this->set_red_if_supported(values.get_red()); - this->set_green_if_supported(values.get_green()); - this->set_blue_if_supported(values.get_blue()); - this->set_white_if_supported(values.get_white()); - this->set_color_temperature_if_supported(values.get_color_temperature()); - return *this; -} -LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { - if (this->parent_->get_traits().get_supports_brightness()) - this->set_transition_length(transition_length); - return *this; -} -LightCall &LightCall::set_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_brightness()) - this->set_brightness(brightness); - return *this; -} -LightCall &LightCall::set_red_if_supported(float red) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_red(red); - return *this; -} -LightCall &LightCall::set_green_if_supported(float green) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_green(green); - return *this; -} -LightCall &LightCall::set_blue_if_supported(float blue) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_blue(blue); - return *this; -} -LightCall &LightCall::set_white_if_supported(float white) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) - this->set_white(white); - return *this; -} -LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { - if (this->parent_->get_traits().get_supports_color_temperature()) - this->set_color_temperature(color_temperature); - return *this; -} -LightCall &LightCall::set_state(optional state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_state(bool state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_transition_length(optional transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_transition_length(uint32_t transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_flash_length(optional flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_flash_length(uint32_t flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_brightness(optional brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_brightness(float brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_red(optional red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_red(float red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_green(optional green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_green(float green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_blue(optional blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_blue(float blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_white(optional white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_white(float white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_color_temperature(optional color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_color_temperature(float color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_effect(optional effect) { - if (effect.has_value()) - this->set_effect(*effect); - return *this; -} -LightCall &LightCall::set_effect(uint32_t effect_number) { - this->effect_ = effect_number; - return *this; -} -LightCall &LightCall::set_effect(optional effect_number) { - this->effect_ = effect_number; - return *this; -} -LightCall &LightCall::set_publish(bool publish) { - this->publish_ = publish; - return *this; -} -LightCall &LightCall::set_save(bool save) { - this->save_ = save; - return *this; -} -LightCall &LightCall::set_rgb(float red, float green, float blue) { - this->set_red(red); - this->set_green(green); - this->set_blue(blue); - return *this; -} -LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) { - this->set_rgb(red, green, blue); - this->set_white(white); - return *this; -} float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +uint32_t LightState::hash_base() { return 1114400283; } + +void LightState::publish_state() { this->remote_values_callback_.call(); } + LightOutput *LightState::get_output() const { return this->output_; } -void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } -void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } -void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness, this->gamma_correct_); -} -void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { - auto traits = this->get_traits(); - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); -} -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { - auto traits = this->get_traits(); - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); -} -void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness, bool color_interlock) { - auto traits = this->get_traits(); - this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, this->gamma_correct_, constant_brightness, - traits.get_supports_color_interlock()); -} -void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - auto traits = this->get_traits(); - this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, - this->gamma_correct_, constant_brightness); +std::string LightState::get_effect_name() { + if (this->active_effect_index_ > 0) + return this->effects_[this->active_effect_index_ - 1]->get_name(); + else + return "None"; } + void LightState::add_new_remote_values_callback(std::function &&send_callback) { this->remote_values_callback_.add(std::move(send_callback)); } @@ -755,12 +158,130 @@ void LightState::add_new_target_state_reached_callback(std::function &&s this->target_state_reached_callback_.add(std::move(send_callback)); } +void LightState::set_default_transition_length(uint32_t default_transition_length) { + this->default_transition_length_ = default_transition_length; +} +uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } +void LightState::set_flash_transition_length(uint32_t flash_transition_length) { + this->flash_transition_length_ = 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; } +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) { + this->effects_.reserve(this->effects_.size() + effects.size()); + for (auto *effect : effects) { + this->effects_.push_back(effect); + } +} + +void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } +void LightState::current_values_as_brightness(float *brightness) { + this->current_values.as_brightness(brightness, this->gamma_correct_); +} +void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); +} +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); +} +void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, + bool constant_brightness) { + this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness); +} +void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, + float *white_brightness) { + auto traits = this->get_traits(); + this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature, + white_brightness, this->gamma_correct_); +} +void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { + auto traits = this->get_traits(); + this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); +} +void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { + auto traits = this->get_traits(); + this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness, + this->gamma_correct_); +} + +void LightState::start_effect_(uint32_t effect_index) { + this->stop_effect_(); + if (effect_index == 0) + return; + + this->active_effect_index_ = effect_index; + auto *effect = this->get_active_effect_(); + effect->start_internal(); +} LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) return nullptr; else return this->effects_[this->active_effect_index_ - 1]; } +void LightState::stop_effect_() { + auto *effect = this->get_active_effect_(); + if (effect != nullptr) { + effect->stop(); + } + this->active_effect_index_ = 0; +} + +void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { + this->transformer_ = this->output_->create_default_transition(); + this->transformer_->setup(this->current_values, target, length); + + if (set_remote_values) { + this->remote_values = target; + } +} + +void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { + LightColorValues end_colors = this->remote_values; + // If starting a flash if one is already happening, set end values to end values of current flash + // Hacky but works + if (this->transformer_ != nullptr) + end_colors = this->transformer_->get_start_values(); + + this->transformer_ = make_unique(*this); + this->transformer_->setup(end_colors, target, length); + + if (set_remote_values) { + this->remote_values = target; + }; +} + +void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { + this->transformer_ = nullptr; + this->current_values = target; + if (set_remote_values) { + this->remote_values = target; + } + this->output_->update_state(this); + this->next_write_ = true; +} + +void LightState::save_remote_values_() { + LightStateRTCState saved; + saved.color_mode = this->remote_values.get_color_mode(); + saved.state = this->remote_values.is_on(); + saved.brightness = this->remote_values.get_brightness(); + saved.color_brightness = this->remote_values.get_color_brightness(); + saved.red = this->remote_values.get_red(); + saved.green = this->remote_values.get_green(); + saved.blue = this->remote_values.get_blue(); + saved.white = this->remote_values.get_white(); + saved.color_temp = this->remote_values.get_color_temperature(); + saved.cold_white = this->remote_values.get_cold_white(); + saved.warm_white = this->remote_values.get_warm_white(); + saved.effect = this->active_effect_index_; + this->rtc_.save(&saved); +} } // namespace light } // namespace esphome diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 10bda2d17b..ae3711234d 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -1,165 +1,20 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" -#include "light_effect.h" +#include "light_call.h" #include "light_color_values.h" +#include "light_effect.h" #include "light_traits.h" #include "light_transformer.h" namespace esphome { namespace light { -class LightState; class LightOutput; -class LightCall { - public: - explicit LightCall(LightState *parent) : parent_(parent) {} - - /// Set the binary ON/OFF state of the light. - LightCall &set_state(optional state); - /// Set the binary ON/OFF state of the light. - LightCall &set_state(bool state); - /** Set the transition length of this call in milliseconds. - * - * This argument is ignored for starting flashes and effects. - * - * Defaults to the default transition length defined in the light configuration. - */ - LightCall &set_transition_length(optional transition_length); - /** Set the transition length of this call in milliseconds. - * - * This argument is ignored for starting flashes and effects. - * - * Defaults to the default transition length defined in the light configuration. - */ - LightCall &set_transition_length(uint32_t transition_length); - /// Set the transition length property if the light supports transitions. - LightCall &set_transition_length_if_supported(uint32_t transition_length); - /// Start and set the flash length of this call in milliseconds. - LightCall &set_flash_length(optional flash_length); - /// Start and set the flash length of this call in milliseconds. - LightCall &set_flash_length(uint32_t flash_length); - /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) - LightCall &set_brightness(optional brightness); - /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) - LightCall &set_brightness(float brightness); - /// Set the brightness property if the light supports brightness. - LightCall &set_brightness_if_supported(float brightness); - /** Set the red RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_red(optional red); - /** Set the red RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_red(float red); - /// Set the red property if the light supports RGB. - LightCall &set_red_if_supported(float red); - /** Set the green RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_green(optional green); - /** Set the green RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_green(float green); - /// Set the green property if the light supports RGB. - LightCall &set_green_if_supported(float green); - /** Set the blue RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_blue(optional blue); - /** Set the blue RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_blue(float blue); - /// Set the blue property if the light supports RGB. - LightCall &set_blue_if_supported(float blue); - /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. - LightCall &set_white(optional white); - /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. - LightCall &set_white(float white); - /// Set the white property if the light supports RGB. - LightCall &set_white_if_supported(float white); - /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. - LightCall &set_color_temperature(optional color_temperature); - /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. - LightCall &set_color_temperature(float color_temperature); - /// Set the color_temperature property if the light supports color temperature. - LightCall &set_color_temperature_if_supported(float color_temperature); - /// Set the effect of the light by its name. - LightCall &set_effect(optional effect); - /// Set the effect of the light by its name. - LightCall &set_effect(const std::string &effect); - /// Set the effect of the light by its internal index number (only for internal use). - LightCall &set_effect(uint32_t effect_number); - LightCall &set_effect(optional effect_number); - /// Set whether this light call should trigger a publish state. - LightCall &set_publish(bool publish); - /// Set whether this light call should trigger a save state to recover them at startup.. - LightCall &set_save(bool save); - - /** Set the RGB color of the light by RGB values. - * - * Please note that this only changes the color of the light, not the brightness. - * - * @param red The red color value from 0.0 to 1.0. - * @param green The green color value from 0.0 to 1.0. - * @param blue The blue color value from 0.0 to 1.0. - * @return The light call for chaining setters. - */ - LightCall &set_rgb(float red, float green, float blue); - /** Set the RGBW color of the light by RGB values. - * - * Please note that this only changes the color of the light, not the brightness. - * - * @param red The red color value from 0.0 to 1.0. - * @param green The green color value from 0.0 to 1.0. - * @param blue The blue color value from 0.0 to 1.0. - * @param white The white color value from 0.0 to 1.0. - * @return The light call for chaining setters. - */ - LightCall &set_rgbw(float red, float green, float blue, float white); -#ifdef USE_JSON - LightCall &parse_color_json(JsonObject &root); - LightCall &parse_json(JsonObject &root); -#endif - LightCall &from_light_color_values(const LightColorValues &values); - - void perform(); - - protected: - /// Validate all properties and return the target light color values. - LightColorValues validate_(); - - bool has_transition_() { return this->transition_length_.has_value(); } - bool has_flash_() { return this->flash_length_.has_value(); } - bool has_effect_() { return this->effect_.has_value(); } - - LightState *parent_; - optional state_; - optional transition_length_; - optional flash_length_; - optional brightness_; - optional red_; - optional green_; - optional blue_; - optional white_; - optional color_temperature_; - optional effect_; - bool publish_{true}; - bool save_{true}; -}; - enum LightRestoreMode { LIGHT_RESTORE_DEFAULT_OFF, LIGHT_RESTORE_DEFAULT_ON, @@ -172,11 +27,13 @@ enum LightRestoreMode { /** This class represents the communication layer between the front-end MQTT layer and the * hardware output layer. */ -class LightState : public Nameable, public Component { +class LightState : public EntityBase, public Component { public: /// Construct this LightState using the provided traits and name. LightState(const std::string &name, LightOutput *output); + LightState(LightOutput *output); + LightTraits get_traits(); /// Make a light state call @@ -200,24 +57,20 @@ class LightState : public Nameable, public Component { * property will be changed continuously (in contrast to .remote_values, where they * are constant during transitions). * + * This value does not have gamma correction applied. + * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues current_values; - /// Deprecated method to access current_values. - ESPDEPRECATED("get_current_values() is deprecated, please use .current_values instead.") - LightColorValues get_current_values(); - - /// Deprecated method to access remote_values. - ESPDEPRECATED("get_remote_values() is deprecated, please use .remote_values instead.") - LightColorValues get_remote_values(); - /** The remote color values reported to the frontend. * * These are different from the "current" values: For example transitions will * continuously change the "current" values. But the remote values will immediately * switch to the target value for a transition, reducing the number of packets sent. * + * This value does not have gamma correction applied. + * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues remote_values; @@ -231,46 +84,47 @@ class LightState : public Nameable, public Component { /// Return the name of the current effect, or if no effect is active "None". std::string get_effect_name(); - /** This lets front-end components subscribe to light change events. - * - * This is different from add_new_current_values_callback in that it only sends events for start - * and end values. For example, with transitions it will only send a single callback whereas - * the callback passed in add_new_current_values_callback will be called every loop() cycle when - * a transition is active - * - * Note the callback should get the output values through get_remote_values(). + /** + * This lets front-end components subscribe to light change events. This callback is called once + * when the remote color values are changed. * * @param send_callback The callback. */ void add_new_remote_values_callback(std::function &&send_callback); /** - * The callback is called once the state of current_values and remote_values are equal + * The callback is called once the state of current_values and remote_values are equal (when the + * transition is finished). * * @param send_callback */ void add_new_target_state_reached_callback(std::function &&send_callback); - /// Return whether the light has any effects that meet the trait requirements. - bool supports_effects(); - -#ifdef USE_JSON - /// Dump the state of this light as JSON. - void dump_json(JsonObject &root); -#endif - /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); + uint32_t get_default_transition_length() const; + + /// Set the flash transition length + void set_flash_transition_length(uint32_t flash_transition_length); + uint32_t get_flash_transition_length() const; /// Set the gamma correction factor void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } - void set_restore_mode(LightRestoreMode restore_mode) { restore_mode_ = restore_mode; } + /// Set the restore mode of this light + void set_restore_mode(LightRestoreMode restore_mode); + + /// Return whether the light has any effects that meet the trait requirements. + bool supports_effects(); + + /// Get all effects for this light state. const std::vector &get_effects() const; + /// Add effects for this light state. void add_effects(const std::vector &effects); + /// The result of all the current_values_as_* methods have gamma correction applied. void current_values_as_binary(bool *binary); void current_values_as_brightness(float *brightness); @@ -280,10 +134,15 @@ class LightState : public Nameable, public Component { void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness = false, bool color_interlock = false); + bool constant_brightness = false); + + void current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, + float *white_brightness); void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); + void current_values_as_ct(float *color_temperature, float *white_brightness); + protected: friend LightOutput; friend LightCall; @@ -293,32 +152,34 @@ class LightState : public Nameable, public Component { /// Internal method to start an effect with the given index void start_effect_(uint32_t effect_index); + /// Internal method to get the currently active effect + LightEffect *get_active_effect_(); /// Internal method to stop the current effect (if one is active). void stop_effect_(); /// Internal method to start a transition to the target color with the given length. - void start_transition_(const LightColorValues &target, uint32_t length); + void start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values); /// Internal method to start a flash for the specified amount of time. - void start_flash_(const LightColorValues &target, uint32_t length); + void start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values); /// Internal method to set the color values to target immediately (with no transition). void set_immediately_(const LightColorValues &target, bool set_remote_values); - /// Internal method to start a transformer. - void set_transformer_(std::unique_ptr transformer); + /// Internal method to save the current remote_values to the preferences + void save_remote_values_(); - LightEffect *get_active_effect_(); - - /// Object used to store the persisted values of the light. - ESPPreferenceObject rtc_; - /// Restore mode of the light. - LightRestoreMode restore_mode_; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; + /// Store the output to allow effects to have more access. + LightOutput *output_; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + + /// Object used to store the persisted values of the light. + ESPPreferenceObject rtc_; + /** Callback to call when new values for the frontend are available. * * "Remote values" are light color values that are reported to the frontend and have a lower @@ -333,11 +194,14 @@ class LightState : public Nameable, public Component { */ CallbackManager target_state_reached_callback_{}; - LightOutput *output_; ///< Store the output to allow effects to have more access. - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; + /// Restore mode of the light. + LightRestoreMode restore_mode_; /// List of effects for this light. std::vector effects_; }; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index ed9c0d44ea..7c99d721f0 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,5 +1,9 @@ #pragma once +#include "esphome/core/helpers.h" +#include "color_mode.h" +#include + namespace esphome { namespace light { @@ -8,35 +12,49 @@ class LightTraits { public: LightTraits() = default; - bool get_supports_brightness() const { return this->supports_brightness_; } - void set_supports_brightness(bool supports_brightness) { this->supports_brightness_ = supports_brightness; } - bool get_supports_rgb() const { return this->supports_rgb_; } - void set_supports_rgb(bool supports_rgb) { this->supports_rgb_ = supports_rgb; } - bool get_supports_rgb_white_value() const { return this->supports_rgb_white_value_; } - void set_supports_rgb_white_value(bool supports_rgb_white_value) { - this->supports_rgb_white_value_ = supports_rgb_white_value; + const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(std::set supported_color_modes) { + this->supported_color_modes_ = std::move(supported_color_modes); } - bool get_supports_color_temperature() const { return this->supports_color_temperature_; } - void set_supports_color_temperature(bool supports_color_temperature) { - this->supports_color_temperature_ = supports_color_temperature; + + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_capability(ColorCapability color_capability) const { + for (auto mode : this->supported_color_modes_) { + if (mode & color_capability) + return true; + } + return false; } - bool get_supports_color_interlock() const { return this->supports_color_interlock_; } - void set_supports_color_interlock(bool supports_color_interlock) { - this->supports_color_interlock_ = supports_color_interlock; + + ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21") + bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); } + ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21") + bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); } + ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21") + bool get_supports_rgb_white_value() const { + return this->supports_color_mode(ColorMode::RGB_WHITE) || + this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE); } + ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21") + bool get_supports_color_temperature() const { + return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE); + } + ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21") + bool get_supports_color_interlock() const { + return this->supports_color_mode(ColorMode::RGB) && + (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) || + this->supports_color_mode(ColorMode::COLOR_TEMPERATURE)); + } + float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: - bool supports_brightness_{false}; - bool supports_rgb_{false}; - bool supports_rgb_white_value_{false}; - bool supports_color_temperature_{false}; + std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; - bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 222be7802c..dd904d0eed 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -1,42 +1,45 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" #include "light_color_values.h" namespace esphome { namespace light { -/// Base-class for all light color transformers, such as transitions or flashes. +/// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { public: - LightTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : start_time_(start_time), length_(length), start_values_(start_values), target_values_(target_values) {} + virtual ~LightTransformer() = default; - LightTransformer() = delete; + void setup(const LightColorValues &start_values, const LightColorValues &target_values, uint32_t length) { + this->start_time_ = millis(); + this->length_ = length; + this->start_values_ = start_values; + this->target_values_ = target_values; + this->start(); + } - /// Whether this transformation is finished - virtual bool is_finished() { return this->get_progress() >= 1.0f; } + /// Indicates whether this transformation is finished. + virtual bool is_finished() { return this->get_progress_() >= 1.0f; } - /// This will be called to get the current values for output. - virtual LightColorValues get_values() = 0; + /// This will be called before the transition is started. + virtual void start() {} - /// The values that should be reported to the front-end. - virtual LightColorValues get_remote_values() { return this->get_target_values_(); } + /// This will be called while the transformer is active to apply the transition to the light. Can either write to the + /// light directly, or return LightColorValues that will be applied. + virtual optional apply() = 0; - /// The values that should be set after this transformation is complete. - virtual LightColorValues get_end_values() { return this->get_target_values_(); } + /// This will be called after transition is finished. + virtual void stop() {} - virtual bool publish_at_end() = 0; - virtual bool is_transition() = 0; + const LightColorValues &get_start_values() const { return this->start_values_; } - float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } + const LightColorValues &get_target_values() const { return this->target_values_; } protected: - const LightColorValues &get_start_values_() const { return this->start_values_; } - - const LightColorValues &get_target_values_() const { return this->target_values_; } + /// The progress of this transition, on a scale of 0 to 1. + float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } uint32_t start_time_; uint32_t length_; @@ -44,46 +47,5 @@ class LightTransformer { LightColorValues target_values_; }; -class LightTransitionTransformer : public LightTransformer { - public: - LightTransitionTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : LightTransformer(start_time, length, start_values, target_values) { - // When turning light on from off state, use colors from new. - if (!this->start_values_.is_on() && this->target_values_.is_on()) { - this->start_values_.set_brightness(0.0f); - this->start_values_.set_red(target_values.get_red()); - this->start_values_.set_green(target_values.get_green()); - this->start_values_.set_blue(target_values.get_blue()); - this->start_values_.set_white(target_values.get_white()); - this->start_values_.set_color_temperature(target_values.get_color_temperature()); - } - } - - LightColorValues get_values() override { - float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); - return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); - } - - bool publish_at_end() override { return false; } - bool is_transition() override { return true; } - - static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } -}; - -class LightFlashTransformer : public LightTransformer { - public: - LightFlashTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : LightTransformer(start_time, length, start_values, target_values) {} - - LightColorValues get_values() override { return this->get_target_values_(); } - - LightColorValues get_end_values() override { return this->get_start_values_(); } - - bool publish_at_end() override { return true; } - bool is_transition() override { return false; } -}; - } // namespace light } // namespace esphome diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h new file mode 100644 index 0000000000..90646f4e61 --- /dev/null +++ b/esphome/components/light/transformers.h @@ -0,0 +1,119 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "light_color_values.h" +#include "light_state.h" +#include "light_transformer.h" + +namespace esphome { +namespace light { + +class LightTransitionTransformer : public LightTransformer { + public: + void start() override { + // When turning light on from off state, use target state and only increase brightness from zero. + if (!this->start_values_.is_on() && this->target_values_.is_on()) { + this->start_values_ = LightColorValues(this->target_values_); + this->start_values_.set_brightness(0.0f); + } + + // When turning light off from on state, use source state and only decrease brightness to zero. + if (this->start_values_.is_on() && !this->target_values_.is_on()) { + this->target_values_ = LightColorValues(this->start_values_); + this->target_values_.set_brightness(0.0f); + } + + // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. + if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->changing_color_mode_ = true; + this->intermediate_values_ = this->start_values_; + this->intermediate_values_.set_state(false); + } + } + + optional apply() override { + float p = this->get_progress_(); + + // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. + if (this->changing_color_mode_ && p > 0.5f && + this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->intermediate_values_ = this->target_values_; + this->intermediate_values_.set_state(false); + } + + LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; + LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + if (this->changing_color_mode_) + p = p < 0.5f ? p * 2 : (p - 0.5) * 2; + + float v = LightTransitionTransformer::smoothed_progress(p); + return LightColorValues::lerp(start, end, v); + } + + protected: + // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like + // transition from 0 to 1 on x = [0, 1] + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + + bool changing_color_mode_{false}; + LightColorValues intermediate_values_{}; +}; + +class LightFlashTransformer : public LightTransformer { + public: + LightFlashTransformer(LightState &state) : state_(state) {} + + void start() override { + this->transition_length_ = this->state_.get_flash_transition_length(); + if (this->transition_length_ * 2 > this->length_) + this->transition_length_ = this->length_ / 2; + + // do not create transition if length is 0 + if (this->transition_length_ == 0) + return; + + // first transition to original target + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_); + } + + optional apply() override { + // transition transformer does not handle 0 length as progress returns nan + if (this->transition_length_ == 0) + return this->target_values_; + + if (this->transformer_ != nullptr) { + if (!this->transformer_->is_finished()) { + return this->transformer_->apply(); + } else { + this->transformer_->stop(); + this->transformer_ = nullptr; + } + } + + if (millis() > this->start_time_ + this->length_ - this->transition_length_) { + // second transition back to start value + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); + } + + // once transition is complete, don't change states until next transition + return optional(); + } + + // Restore the original values after the flash. + void stop() override { + this->state_.current_values = this->get_start_values(); + this->state_.remote_values = this->get_start_values(); + this->state_.publish_state(); + } + + protected: + LightState &state_; + uint32_t transition_length_; + std::unique_ptr transformer_{nullptr}; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 7c96cda7b1..cf544e5435 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -3,7 +3,7 @@ from esphome import automation # Base light_ns = cg.esphome_ns.namespace("light") -LightState = light_ns.class_("LightState", cg.Nameable, cg.Component) +LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component) # Fake class for addressable lights AddressableLightState = light_ns.class_("LightState", LightState) LightOutput = light_ns.class_("LightOutput") @@ -13,6 +13,20 @@ AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") +# Color modes +ColorMode = light_ns.enum("ColorMode", is_class=True) +COLOR_MODES = { + "ON_OFF": ColorMode.ON_OFF, + "BRIGHTNESS": ColorMode.BRIGHTNESS, + "WHITE": ColorMode.WHITE, + "COLOR_TEMPERATURE": ColorMode.COLOR_TEMPERATURE, + "COLD_WARM_WHITE": ColorMode.COLD_WARM_WHITE, + "RGB": ColorMode.RGB, + "RGB_WHITE": ColorMode.RGB_WHITE, + "RGB_COLOR_TEMPERATURE": ColorMode.RGB_COLOR_TEMPERATURE, + "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 2b571b817b..bc1bc6bb41 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -7,6 +7,7 @@ from esphome.automation import LambdaAction from esphome.const import ( CONF_ARGS, CONF_BAUD_RATE, + CONF_DEASSERT_RTS_DTR, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, @@ -85,8 +86,7 @@ def validate_local_no_higher_than_global(value): for tag, level in value.get(CONF_LOGS, {}).items(): if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level): raise EsphomeError( - "The local log level {} for {} must be less severe than the " - "global log level {}.".format(level, tag, global_level) + f"The local log level {level} for {tag} must be less severe than the global log level {global_level}." ) return value @@ -104,6 +104,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(Logger), cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, + cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection, cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, cv.Optional(CONF_LOGS, default={}): cv.Schema( @@ -143,7 +144,7 @@ async def to_code(config): level = config[CONF_LEVEL] cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) - cg.add_build_flag("-DESPHOME_LOG_LEVEL={}".format(LOG_LEVELS[level])) + cg.add_build_flag(f"-DESPHOME_LOG_LEVEL={LOG_LEVELS[level]}") verbose_severity = LOG_LEVEL_SEVERITY.index("VERBOSE") very_verbose_severity = LOG_LEVEL_SEVERITY.index("VERY_VERBOSE") @@ -205,8 +206,7 @@ def maybe_simple_message(schema): def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python - # pylint: disable=anomalous-backslash-in-string - cfmt = """\ + cfmt = r""" ( # start of capture group 1 % # literal "%" (?:[-+0 #]{0,5}) # optional flags @@ -214,13 +214,12 @@ def validate_printf(value): (?:\.(?:\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( - "Found {} printf-patterns ({}), but {} args were given!" - "".format(len(matches), ", ".join(matches), len(value[CONF_ARGS])) + f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" ) return value diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ce82a51b94..2d85969bf3 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -1,9 +1,15 @@ #include "logger.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP_IDF +#include "freertos/FreeRTOS.h" +#include +#endif + +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) #include #endif -#include +#include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace logger { @@ -43,28 +49,31 @@ void Logger::write_header_(int level, const char *tag, int line) { } void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); this->write_header_(level, tag, line); this->vprintf_to_buffer_(format, args); this->write_footer_(); this->log_message_(level, tag); + recursion_guard_ = false; } #ifdef USE_STORE_LOG_STR_IN_FLASH void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); // copy format string - const char *format_pgm_p = (PGM_P) format; + auto *format_pgm_p = reinterpret_cast(format); size_t len = 0; char ch = '.'; while (!this->is_buffer_full_() && ch != '\0') { - this->tx_buffer_[this->tx_buffer_at_++] = ch = pgm_read_byte(format_pgm_p++); + this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); } // Buffer full form copying format if (this->is_buffer_full_()) @@ -78,6 +87,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr this->vprintf_to_buffer_(this->tx_buffer_, args); this->write_footer_(); this->log_message_(level, tag, offset); + recursion_guard_ = false; } #endif @@ -101,32 +111,41 @@ 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) + if (this->baud_rate_ > 0) { +#ifdef USE_ARDUINO this->hw_serial_->println(msg); -#ifdef ARDUINO_ARCH_ESP32 +#endif // USE_ARDUINO +#ifdef USE_ESP_IDF + uart_write_bytes(uart_num_, msg, strlen(msg)); + uart_write_bytes(uart_num_, "\n", 1); +#endif + } + +#ifdef USE_ESP32 // Suppress network-logging if memory constrained, but still log to serial // ports. In some configurations (eg BLE enabled) there may be some transient // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping // here usually allows the stack to recover instead. // See issue #1234 for analysis. - if (xPortGetFreeHeapSize() > 2048) - this->log_callback_.call(level, tag, msg); -#else - this->log_callback_.call(level, tag, msg); + if (xPortGetFreeHeapSize() < 2048) + return; #endif + + this->log_callback_.call(level, tag, msg); } Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) { // add 1 to buffer size for null terminator - this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; + this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT } void Logger::pre_setup() { if (this->baud_rate_ > 0) { +#ifdef USE_ARDUINO switch (this->uart_) { case UART_SELECTION_UART0: -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 case UART_SELECTION_UART0_SWAP: #endif this->hw_serial_ = &Serial; @@ -134,7 +153,7 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; break; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 case UART_SELECTION_UART2: #if !CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_IDF_TARGET_ESP32C3 // FIXME: Validate in config that UART2 can't be set for ESP32-S2 (only has @@ -144,23 +163,50 @@ void Logger::pre_setup() { 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; + case UART_SELECTION_UART2: + uart_num_ = UART_NUM_2; + break; + } + 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 +#ifdef USE_ARDUINO this->hw_serial_->begin(this->baud_rate_); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { this->hw_serial_->swap(); } this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); #endif +#endif // USE_ARDUINO } -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 else { uart_set_debug(UART_NO); } #endif global_logger = this; -#ifdef ARDUINO_ARCH_ESP32 +#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); @@ -179,10 +225,10 @@ void Logger::add_on_log_callback(std::function + +#ifdef USE_ARDUINO +#include +#endif +#ifdef USE_ESP_IDF +#include +#endif namespace esphome { @@ -17,11 +24,11 @@ namespace logger { enum UARTSelection { UART_SELECTION_UART0 = 0, UART_SELECTION_UART1, -#ifdef ARDUINO_ARCH_ESP32 - UART_SELECTION_UART2 +#ifdef USE_ESP32 + UART_SELECTION_UART2, #endif -#ifdef ARDUINO_ARCH_ESP8266 - UART_SELECTION_UART0_SWAP +#ifdef USE_ESP8266 + UART_SELECTION_UART0_SWAP, #endif }; @@ -32,7 +39,12 @@ class Logger : public Component { /// 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_; } +#ifdef USE_ARDUINO HardwareSerial *get_hw_serial() const { return hw_serial_; } +#endif +#ifdef USE_ESP_IDF + uart_port_t get_uart_num() const { return uart_num_; } +#endif /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -106,13 +118,20 @@ class Logger : public Component { int tx_buffer_at_{0}; int tx_buffer_size_{0}; UARTSelection uart_{UART_SELECTION_UART0}; +#ifdef USE_ARDUINO HardwareSerial *hw_serial_{nullptr}; +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; +#endif struct LogLevelOverride { std::string tag; int level; }; std::vector log_levels_; CallbackManager log_callback_{}; + /// Prevents recursive log calls, if true a log message is already being processed. + bool recursion_guard_ = false; }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/ltr390/__init__.py b/esphome/components/ltr390/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp new file mode 100644 index 0000000000..36f3835724 --- /dev/null +++ b/esphome/components/ltr390/ltr390.cpp @@ -0,0 +1,166 @@ +#include "ltr390.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace ltr390 { + +static const char *const TAG = "ltr390"; + +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 uint32_t MODEADDRESSES[2] = {0x0D, 0x10}; + +uint32_t little_endian_bytes_to_int(const uint8_t *buffer, uint8_t num_bytes) { + uint32_t value = 0; + + for (int i = 0; i < num_bytes; i++) { + value <<= 8; + value |= buffer[num_bytes - i - 1]; + } + + return value; +} + +optional LTR390Component::read_sensor_data_(LTR390MODE mode) { + const uint8_t num_bytes = 3; + uint8_t buffer[num_bytes]; + + // Wait until data available + const uint32_t now = millis(); + while (true) { + std::bitset<8> status = this->reg(LTR390_MAIN_STATUS).get(); + bool available = status[3]; + if (available) + break; + + if (millis() - now > 100) { + ESP_LOGW(TAG, "Sensor didn't return any data, aborting"); + return {}; + } + ESP_LOGD(TAG, "Waiting for data"); + delay(2); + } + + if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) { + ESP_LOGW(TAG, "Reading data from sensor failed!"); + return {}; + } + + return little_endian_bytes_to_int(buffer, num_bytes); +} + +void LTR390Component::read_als_() { + auto val = this->read_sensor_data_(LTR390_MODE_ALS); + if (!val.has_value()) + return; + uint32_t als = *val; + + if (this->light_sensor_ != nullptr) { + float lux = (0.6 * als) / (GAINVALUES[this->gain_] * RESOLUTIONVALUE[this->res_]) * this->wfac_; + this->light_sensor_->publish_state(lux); + } + + if (this->als_sensor_ != nullptr) { + this->als_sensor_->publish_state(als); + } +} + +void LTR390Component::read_uvs_() { + auto val = this->read_sensor_data_(LTR390_MODE_UVS); + if (!val.has_value()) + return; + uint32_t uv = *val; + + if (this->uvi_sensor_ != nullptr) { + this->uvi_sensor_->publish_state(uv / LTR390_SENSITIVITY * this->wfac_); + } + + if (this->uv_sensor_ != nullptr) { + this->uv_sensor_->publish_state(uv); + } +} + +void LTR390Component::read_mode_(int mode_index) { + // Set mode + LTR390MODE mode = std::get<0>(this->mode_funcs_[mode_index]); + + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_MODE] = mode; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + + // After the sensor integration time do the following + this->set_timeout(((uint32_t) RESOLUTIONVALUE[this->res_]) * 100, [this, mode_index]() { + // Read from the sensor + std::get<1>(this->mode_funcs_[mode_index])(); + + // If there are more modes to read then begin the next + // otherwise stop + if (mode_index + 1 < this->mode_funcs_.size()) { + this->read_mode_(mode_index + 1); + } else { + this->reading_ = false; + } + }); +} + +void LTR390Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ltr390..."); + + // reset + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_RST] = true; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + delay(10); + + // Enable + ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_EN] = true; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + + // check enabled + ctrl = this->reg(LTR390_MAIN_CTRL).get(); + bool enabled = ctrl[LTR390_CTRL_EN]; + + if (!enabled) { + ESP_LOGW(TAG, "Sensor didn't respond with enabled state"); + this->mark_failed(); + 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 sensor read state + this->reading_ = false; + + // If we need the light sensor then add to the list + if (this->light_sensor_ != nullptr || this->als_sensor_ != nullptr) { + this->mode_funcs_.emplace_back(LTR390_MODE_ALS, std::bind(<R390Component::read_als_, this)); + } + + // If we need the UV sensor then add to the list + if (this->uvi_sensor_ != nullptr || this->uv_sensor_ != nullptr) { + this->mode_funcs_.emplace_back(LTR390_MODE_UVS, std::bind(<R390Component::read_uvs_, this)); + } +} + +void LTR390Component::dump_config() { LOG_I2C_DEVICE(this); } + +void LTR390Component::update() { + if (!this->reading_ && !mode_funcs_.empty()) { + this->reading_ = true; + this->read_mode_(0); + } +} + +} // namespace ltr390 +} // namespace esphome diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h new file mode 100644 index 0000000000..d607a3e55f --- /dev/null +++ b/esphome/components/ltr390/ltr390.h @@ -0,0 +1,93 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include + +namespace esphome { +namespace ltr390 { + +enum LTR390CTRL { + LTR390_CTRL_EN = 1, + LTR390_CTRL_MODE = 3, + LTR390_CTRL_RST = 4, +}; + +// 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, + LTR390_MODE_UVS, +}; + +// Sensor gain levels +enum LTR390GAIN { + LTR390_GAIN_1 = 0, + LTR390_GAIN_3, // Default + LTR390_GAIN_6, + LTR390_GAIN_9, + LTR390_GAIN_18, +}; + +// Sensor resolution +enum LTR390RESOLUTION { + LTR390_RESOLUTION_20BIT, + LTR390_RESOLUTION_19BIT, + LTR390_RESOLUTION_18BIT, // Default + LTR390_RESOLUTION_17BIT, + LTR390_RESOLUTION_16BIT, + LTR390_RESOLUTION_13BIT, +}; + +class LTR390Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + 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_wfac_value(float wfac) { this->wfac_ = wfac; } + + void set_light_sensor(sensor::Sensor *light_sensor) { this->light_sensor_ = light_sensor; } + void set_als_sensor(sensor::Sensor *als_sensor) { this->als_sensor_ = als_sensor; } + void set_uvi_sensor(sensor::Sensor *uvi_sensor) { this->uvi_sensor_ = uvi_sensor; } + void set_uv_sensor(sensor::Sensor *uv_sensor) { this->uv_sensor_ = uv_sensor; } + + protected: + optional read_sensor_data_(LTR390MODE mode); + + void read_als_(); + void read_uvs_(); + + void read_mode_(int mode_index); + + bool reading_; + + // a list of modes and corresponding read functions + std::vector>> mode_funcs_; + + LTR390GAIN gain_; + LTR390RESOLUTION res_; + float wfac_; + + sensor::Sensor *light_sensor_{nullptr}; + sensor::Sensor *als_sensor_{nullptr}; + + sensor::Sensor *uvi_sensor_{nullptr}; + sensor::Sensor *uv_sensor_{nullptr}; +}; + +} // namespace ltr390 +} // namespace esphome diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py new file mode 100644 index 0000000000..0e70f7bb1b --- /dev/null +++ b/esphome/components/ltr390/sensor.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_GAIN, + CONF_LIGHT, + CONF_RESOLUTION, + UNIT_LUX, + ICON_BRIGHTNESS_5, + DEVICE_CLASS_ILLUMINANCE, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +ltr390_ns = cg.esphome_ns.namespace("ltr390") + +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" + +UNIT_COUNTS = "#" +UNIT_UVI = "UVI" + +LTR390GAIN = ltr390_ns.enum("LTR390GAIN") +GAIN_OPTIONS = { + "X1": LTR390GAIN.LTR390_GAIN_1, + "X3": LTR390GAIN.LTR390_GAIN_3, + "X6": LTR390GAIN.LTR390_GAIN_6, + "X9": LTR390GAIN.LTR390_GAIN_9, + "X18": LTR390GAIN.LTR390_GAIN_18, +} + +LTR390RESOLUTION = ltr390_ns.enum("LTR390RESOLUTION") +RES_OPTIONS = { + 20: LTR390RESOLUTION.LTR390_RESOLUTION_20BIT, + 19: LTR390RESOLUTION.LTR390_RESOLUTION_19BIT, + 18: LTR390RESOLUTION.LTR390_RESOLUTION_18BIT, + 17: LTR390RESOLUTION.LTR390_RESOLUTION_17BIT, + 16: LTR390RESOLUTION.LTR390_RESOLUTION_16BIT, + 13: LTR390RESOLUTION.LTR390_RESOLUTION_13BIT, +} + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LTR390Component), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + UNIT_LUX, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + ), + cv.Optional(CONF_AMBIENT_LIGHT): sensor.sensor_schema( + UNIT_COUNTS, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + ), + cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( + UNIT_UVI, ICON_BRIGHTNESS_5, 5, DEVICE_CLASS_ILLUMINANCE + ), + cv.Optional(CONF_UV): sensor.sensor_schema( + UNIT_COUNTS, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + ), + 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 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x53)), + cv.has_at_least_one_key(CONF_LIGHT, CONF_AMBIENT_LIGHT, CONF_UV_INDEX, CONF_UV), +) + +TYPES = { + CONF_LIGHT: "set_light_sensor", + CONF_AMBIENT_LIGHT: "set_als_sensor", + CONF_UV_INDEX: "set_uvi_sensor", + CONF_UV: "set_uv_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) + + 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)) diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index a8b5d25c61..c7732dfbe3 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_REFERENCE_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,16 +15,19 @@ MAX31855Sensor = max31855_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE) + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ) .extend( { cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 9b627e21a2..9300916fdc 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -163,7 +163,7 @@ void MAX31856Sensor::write_register_(uint8_t reg, uint8_t value) { ESP_LOGV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); } -const uint8_t MAX31856Sensor::read_register_(uint8_t reg) { +uint8_t MAX31856Sensor::read_register_(uint8_t reg) { ESP_LOGVV(TAG, "read_register_ 0x%02X", reg); this->enable(); ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); @@ -175,7 +175,7 @@ const uint8_t MAX31856Sensor::read_register_(uint8_t reg) { return value; } -const uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { +uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { ESP_LOGVV(TAG, "read_register_24_ 0x%02X", reg); this->enable(); ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 779eb52c8e..157aad433c 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -82,8 +82,8 @@ class MAX31856Sensor : public sensor::Sensor, protected: MAX31856ConfigFilter filter_; - const uint8_t read_register_(uint8_t reg); - const uint32_t read_register24_(uint8_t reg); + uint8_t read_register_(uint8_t reg); + uint32_t read_register24_(uint8_t reg); void write_register_(uint8_t reg, uint8_t value); void one_shot_temperature_(); diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 9583c0bcf9..083d2ac30c 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_MAINS_FILTER, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -23,11 +22,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index daadc26cdc..91946cde2c 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -156,7 +156,7 @@ void MAX31865Sensor::write_register_(uint8_t reg, uint8_t value) { ESP_LOGVV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); } -const uint8_t MAX31865Sensor::read_register_(uint8_t reg) { +uint8_t MAX31865Sensor::read_register_(uint8_t reg) { this->enable(); this->write_byte(reg); const uint8_t value(this->read_byte()); @@ -165,7 +165,7 @@ const uint8_t MAX31865Sensor::read_register_(uint8_t reg) { return value; } -const uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { +uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { this->enable(); this->write_byte(reg); const uint8_t msb(this->read_byte()); @@ -176,7 +176,7 @@ const uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { return value; } -float MAX31865Sensor::calc_temperature_(const float &rtd_ratio) { +float MAX31865Sensor::calc_temperature_(float rtd_ratio) { // Based loosely on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31865 // Mainly based on formulas provided by Analog: // http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index 393dd4b434..b83753a678 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -49,9 +49,9 @@ class MAX31865Sensor : public sensor::Sensor, void read_data_(); void write_config_(uint8_t mask, uint8_t bits, uint8_t start_position = 0); void write_register_(uint8_t reg, uint8_t value); - const uint8_t read_register_(uint8_t reg); - const uint16_t read_register_16_(uint8_t reg); - float calc_temperature_(const float &rtd_ratio); + uint8_t read_register_(uint8_t reg); + uint16_t read_register_16_(uint8_t reg); + float calc_temperature_(float rtd_ratio); }; } // namespace max31865 diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index 64495ebd7a..33d9c42be3 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -26,7 +25,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index ad0e89c028..dff8360226 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor, spi from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,7 +15,10 @@ MAX6675Sensor = max6675_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 91bea22e46..960ac58071 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -1,6 +1,7 @@ #include "max7219.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace max7219 { @@ -117,7 +118,7 @@ float MAX7219Component::get_setup_priority() const { return setup_priority::PROC void MAX7219Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MAX7219..."); this->spi_setup(); - this->buffer_ = new uint8_t[this->num_chips_ * 8]; + this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT for (uint8_t i = 0; i < this->num_chips_ * 8; i++) this->buffer_[i] = 0; @@ -172,7 +173,7 @@ uint8_t MAX7219Component::print(uint8_t start_pos, const char *str) { for (; *str != '\0'; str++) { uint8_t data = MAX7219_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') - data = pgm_read_byte(&MAX7219_ASCII_TO_RAW[*str - ' ']); + data = progmem_read_byte(&MAX7219_ASCII_TO_RAW[*str - ' ']); if (data == MAX7219_UNKNOWN_CHAR) { ESP_LOGW(TAG, "Encountered character '%c' with no MAX7219 representation while translating string!", *str); diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index b130823c12..4fedd3d312 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -1,6 +1,7 @@ #include "max7219digit.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" #include "max7219font.h" namespace esphome { @@ -104,7 +105,7 @@ void MAX7219Component::display() { uint8_t pixels[8]; // Run this loop for every MAX CHIP (GRID OF 64 leds) // Run this routine for the rows of every chip 8x row 0 top to 7 bottom - // Fill the pixel parameter with diplay data + // Fill the pixel parameter with display data // Send the data to the chip for (uint8_t i = 0; i < this->num_chips_; i++) { for (uint8_t j = 0; j < 8; j++) { @@ -119,7 +120,7 @@ void MAX7219Component::display() { } int MAX7219Component::get_height_internal() { - return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER // TO BE DONE -> CREATE Virtual size of screen and scroll } @@ -212,7 +213,7 @@ void MAX7219Component::scroll_left() { void MAX7219Component::send_char(uint8_t chip, uint8_t data) { // get this character from PROGMEM for (uint8_t i = 0; i < 8; i++) - this->max_displaybuffer_[chip * 8 + i] = pgm_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); + this->max_displaybuffer_[chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); } // end of send_char // send one character (data) to position (chip) @@ -238,7 +239,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } else { b = pixels[7 - col]; } - // send this byte to dispay at selected chip + // send this byte to display at selected chip if (this->invert_) { this->send_byte_(col + 1, ~b); } else { diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 83f45e3a00..02fe8b6f42 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -54,8 +54,8 @@ class MAX7219Component : public PollingComponent, void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; void set_reverse(bool on_off) { this->reverse_ = on_off; }; - void send_char(byte chip, byte data); - void send64pixels(byte chip, const byte pixels[8]); + void send_char(uint8_t chip, uint8_t data); + void send64pixels(uint8_t chip, const uint8_t pixels[8]); void scroll_left(); void scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 3d42d1cc2d..22d64d1ecd 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -1,11 +1,13 @@ #pragma once +#include "esphome/core/hal.h" + namespace esphome { namespace max7219digit { // bit patterns for the CP437 font -const byte MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { +const uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h index 42c8e497fa..406ce0b419 100644 --- a/esphome/components/mcp23008/mcp23008.h +++ b/esphome/components/mcp23008/mcp23008.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23x08_base/mcp23x08_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 4d9657e794..c1209a9627 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -2,17 +2,19 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import i2c -from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, +) DEPENDENCIES = ["i2c"] MULTI_CONF = True mcp23016_ns = cg.esphome_ns.namespace("mcp23016") -MCP23016GPIOMode = mcp23016_ns.enum("MCP23016GPIOMode") -MCP23016_GPIO_MODES = { - "INPUT": MCP23016GPIOMode.MCP23016_INPUT, - "OUTPUT": MCP23016GPIOMode.MCP23016_OUTPUT, -} MCP23016 = mcp23016_ns.class_("MCP23016", cg.Component, i2c.I2CDevice) MCP23016GPIOPin = mcp23016_ns.class_("MCP23016GPIOPin", cg.GPIOPin) @@ -34,34 +36,41 @@ async def to_code(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 + + CONF_MCP23016 = "mcp23016" -MCP23016_OUTPUT_PIN_SCHEMA = cv.Schema( +MCP23016_PIN_SCHEMA = cv.All( { + cv.GenerateID(): cv.declare_id(MCP23016GPIOPin), cv.Required(CONF_MCP23016): cv.use_id(MCP23016), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum( - MCP23016_GPIO_MODES, upper=True - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) -MCP23016_INPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_MCP23016): cv.use_id(MCP23016), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum( - MCP23016_GPIO_MODES, upper=True + 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, } ) -@pins.PIN_SCHEMA_REGISTRY.register( - CONF_MCP23016, (MCP23016_OUTPUT_PIN_SCHEMA, MCP23016_INPUT_PIN_SCHEMA) -) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23016, MCP23016_PIN_SCHEMA) async def mcp23016_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_MCP23016]) - return MCP23016GPIOPin.new( - parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED] - ) + + 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/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index f2b55fe2e2..a8df4e1745 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -1,5 +1,6 @@ #include "mcp23016.h" #include "esphome/core/log.h" +#include namespace esphome { namespace mcp23016 { @@ -29,17 +30,12 @@ void MCP23016::digital_write(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; this->update_reg_(pin, value, reg_addr); } -void MCP23016::pin_mode(uint8_t pin, uint8_t mode) { +void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1; - switch (mode) { - case MCP23016_INPUT: - this->update_reg_(pin, true, iodir); - break; - case MCP23016_OUTPUT: - this->update_reg_(pin, false, iodir); - break; - default: - break; + if (flags == gpio::FLAG_INPUT) { + this->update_reg_(pin, true, iodir); + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_reg_(pin, false, iodir); } } float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -80,12 +76,15 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { } } -MCP23016GPIOPin::MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted) - : GPIOPin(pin, mode, inverted), parent_(parent) {} -void MCP23016GPIOPin::setup() { this->pin_mode(this->mode_); } -void MCP23016GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +void MCP23016GPIOPin::setup() { pin_mode(flags_); } +void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string MCP23016GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_); + return buffer; +} } // namespace mcp23016 } // namespace esphome diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 53502f80eb..a4890b4120 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -1,18 +1,12 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { namespace mcp23016 { -/// Modes for MCP23016 pins -enum MCP23016GPIOMode : uint8_t { - MCP23016_INPUT = INPUT, // 0x00 - MCP23016_OUTPUT = OUTPUT // 0x01 -}; - enum MCP23016GPIORegisters { // 0 side MCP23016_GP0 = 0x00, @@ -38,7 +32,7 @@ class MCP23016 : public Component, public i2c::I2CDevice { bool digital_read(uint8_t pin); void digital_write(uint8_t pin, bool value); - void pin_mode(uint8_t pin, uint8_t mode); + void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; @@ -56,15 +50,22 @@ class MCP23016 : public Component, public i2c::I2CDevice { class MCP23016GPIOPin : public GPIOPin { public: - MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted = false); - void setup() override; - void pin_mode(uint8_t mode) 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(MCP23016 *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } protected: MCP23016 *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; }; } // namespace mcp23016 diff --git a/esphome/components/mcp23017/mcp23017.h b/esphome/components/mcp23017/mcp23017.h index fd9086a492..8959e06a41 100644 --- a/esphome/components/mcp23017/mcp23017.h +++ b/esphome/components/mcp23017/mcp23017.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23x17_base/mcp23x17_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { diff --git a/esphome/components/mcp23s08/mcp23s08.h b/esphome/components/mcp23s08/mcp23s08.h index 4ca02c54fc..a2a6be880a 100644 --- a/esphome/components/mcp23s08/mcp23s08.h +++ b/esphome/components/mcp23s08/mcp23s08.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23x08_base/mcp23x08_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" namespace esphome { diff --git a/esphome/components/mcp23s17/mcp23s17.h b/esphome/components/mcp23s17/mcp23s17.h index 1ced144c23..cb5d6cfcd8 100644 --- a/esphome/components/mcp23s17/mcp23s17.h +++ b/esphome/components/mcp23s17/mcp23s17.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23x17_base/mcp23x17_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" namespace esphome { diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 2b047fa288..2137b36921 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -19,22 +19,16 @@ void MCP23X08Base::digital_write(uint8_t pin, bool value) { this->update_reg(pin, value, reg_addr); } -void MCP23X08Base::pin_mode(uint8_t pin, uint8_t mode) { +void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = mcp23x08_base::MCP23X08_IODIR; uint8_t gppu = mcp23x08_base::MCP23X08_GPPU; - switch (mode) { - case mcp23xxx_base::MCP23XXX_INPUT: - this->update_reg(pin, true, iodir); - break; - case mcp23xxx_base::MCP23XXX_INPUT_PULLUP: - this->update_reg(pin, true, iodir); - this->update_reg(pin, true, gppu); - break; - case mcp23xxx_base::MCP23XXX_OUTPUT: - this->update_reg(pin, false, iodir); - break; - default: - break; + if (flags == gpio::FLAG_INPUT) { + this->update_reg(pin, true, iodir); + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + this->update_reg(pin, true, iodir); + this->update_reg(pin, true, gppu); + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_reg(pin, false, iodir); } } diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.h b/esphome/components/mcp23x08_base/mcp23x08_base.h index 5e2c1a047f..910519119b 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.h +++ b/esphome/components/mcp23x08_base/mcp23x08_base.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace mcp23x08_base { @@ -26,7 +26,7 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase { public: bool digital_read(uint8_t pin) override; void digital_write(uint8_t pin, bool value) override; - void pin_mode(uint8_t pin, uint8_t mode) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; protected: diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 72dec2d457..e975670faa 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -19,22 +19,16 @@ void MCP23X17Base::digital_write(uint8_t pin, bool value) { this->update_reg(pin, value, reg_addr); } -void MCP23X17Base::pin_mode(uint8_t pin, uint8_t mode) { +void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = pin < 8 ? mcp23x17_base::MCP23X17_IODIRA : mcp23x17_base::MCP23X17_IODIRB; uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB; - switch (mode) { - case mcp23xxx_base::MCP23XXX_INPUT: - this->update_reg(pin, true, iodir); - break; - case mcp23xxx_base::MCP23XXX_INPUT_PULLUP: - this->update_reg(pin, true, iodir); - this->update_reg(pin, true, gppu); - break; - case mcp23xxx_base::MCP23XXX_OUTPUT: - this->update_reg(pin, false, iodir); - break; - default: - break; + if (flags == gpio::FLAG_INPUT) { + this->update_reg(pin, true, iodir); + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + this->update_reg(pin, true, iodir); + this->update_reg(pin, true, gppu); + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_reg(pin, false, iodir); } } diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.h b/esphome/components/mcp23x17_base/mcp23x17_base.h index 1bbcb97041..3d50ee8c03 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.h +++ b/esphome/components/mcp23x17_base/mcp23x17_base.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace mcp23x17_base { @@ -38,7 +38,7 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase { public: bool digital_read(uint8_t pin) override; void digital_write(uint8_t pin, bool value) override; - void pin_mode(uint8_t pin, uint8_t mode) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; protected: diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 019b7c7e64..f2c2706416 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -3,11 +3,14 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import ( CONF_ID, + CONF_INPUT, CONF_NUMBER, CONF_MODE, CONF_INVERTED, CONF_INTERRUPT, CONF_OPEN_DRAIN_INTERRUPT, + CONF_OUTPUT, + CONF_PULLUP, ) from esphome.core import coroutine @@ -47,26 +50,29 @@ async def register_mcp23xxx(config): return var +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") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + CONF_MCP23XXX = "mcp23xxx" -MCP23XXX_OUTPUT_PIN_SCHEMA = cv.Schema( +MCP23XXX_PIN_SCHEMA = cv.All( { + cv.GenerateID(): cv.declare_id(MCP23XXXGPIOPin), cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum( - MCP23XXX_GPIO_MODES, upper=True - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( - MCP23XXX_INTERRUPT_MODES, upper=True - ), - } -) -MCP23XXX_INPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum( - MCP23XXX_GPIO_MODES, upper=True + 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( @@ -76,41 +82,31 @@ MCP23XXX_INPUT_PIN_SCHEMA = cv.Schema( ) -@pins.PIN_SCHEMA_REGISTRY.register( - CONF_MCP23XXX, (MCP23XXX_OUTPUT_PIN_SCHEMA, MCP23XXX_INPUT_PIN_SCHEMA) -) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA) async def mcp23xxx_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_MCP23XXX]) - return MCP23XXXGPIOPin.new( - parent, - config[CONF_NUMBER], - config[CONF_MODE], - config[CONF_INVERTED], - config[CONF_INTERRUPT], - ) + + 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]))) + cg.add(var.set_interrupt_mode(config[CONF_INTERRUPT])) + return var # BEGIN Removed pin schemas below to show error in configuration -# TODO remove in 1.19.0 +# TODO remove in 2022.5.0 for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: - PIN_SCHEMA = cv.Schema( - { - cv.Required(id): cv.invalid( - f"'{id}:' has been removed from the pin schema in 1.17.0, please use 'mcp23xxx:'" - ), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum( - MCP23XXX_GPIO_MODES, upper=True - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( - MCP23XXX_INTERRUPT_MODES, upper=True - ), - } + invalid_schema = cv.invalid( + f"'{id}:' has been removed from the pin schema in 1.17.0, please use 'mcp23xxx:'" ) - @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA)) + # pylint: disable=cell-var-from-loop + @pins.PIN_SCHEMA_REGISTRY.register(id, invalid_schema) def pin_to_code(config): pass diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 37c55fceaf..14a703fb9f 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,16 +6,15 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -MCP23XXXGPIOPin::MCP23XXXGPIOPin(MCP23XXXBase *parent, uint8_t pin, uint8_t mode, bool inverted, - MCP23XXXInterruptMode interrupt_mode) - : GPIOPin(pin, mode, inverted), parent_(parent), interrupt_mode_(interrupt_mode) {} -void MCP23XXXGPIOPin::setup() { this->pin_mode(this->mode_); } -void MCP23XXXGPIOPin::pin_mode(uint8_t mode) { - this->parent_->pin_mode(this->pin_, mode); - this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); -} +void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string MCP23XXXGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via MCP23XXX", pin_); + return buffer; +} } // namespace mcp23xxx_base } // namespace esphome diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index bf01320264..a522ea28c5 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -1,25 +1,18 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace mcp23xxx_base { enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; -/// Modes for MCP23XXX pins -enum MCP23XXXGPIOMode : uint8_t { - MCP23XXX_INPUT = INPUT, // 0x00 - MCP23XXX_INPUT_PULLUP = INPUT_PULLUP, // 0x02 - MCP23XXX_OUTPUT = OUTPUT // 0x01 -}; - class MCP23XXXBase : public Component { public: virtual bool digital_read(uint8_t pin); virtual void digital_write(uint8_t pin, bool value); - virtual void pin_mode(uint8_t pin, uint8_t mode); + virtual void pin_mode(uint8_t pin, gpio::Flags flags); virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } @@ -38,16 +31,23 @@ class MCP23XXXBase : public Component { class MCP23XXXGPIOPin : public GPIOPin { public: - MCP23XXXGPIOPin(MCP23XXXBase *parent, uint8_t pin, uint8_t mode, bool inverted = false, - MCP23XXXInterruptMode interrupt_mode = MCP23XXX_NO_INTERRUPT); - void setup() override; - void pin_mode(uint8_t mode) 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(MCP23XXXBase *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + void set_interrupt_mode(MCP23XXXInterruptMode interrupt_mode) { interrupt_mode_ = interrupt_mode; } protected: MCP23XXXBase *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; MCP23XXXInterruptMode interrupt_mode_; }; diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py index 24a48664c1..431963acfd 100644 --- a/esphome/components/mcp3008/__init__.py +++ b/esphome/components/mcp3008/__init__.py @@ -2,6 +2,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.core import CORE DEPENDENCIES = ["spi"] AUTO_LOAD = ["sensor"] @@ -23,3 +24,6 @@ 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 CORE.is_esp32: + cg.add_library("SPI", None) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 909a6f4708..81abc4f012 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -37,11 +37,8 @@ float MCP3008::read_data(uint8_t pin) { return data / 1023.0f; } -MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, const std::string &name, uint8_t pin, float reference_voltage) - : PollingComponent(1000), parent_(parent), pin_(pin) { - this->set_name(name); - this->reference_voltage_ = reference_voltage; -} +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; } diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index 16f1c14fcb..5d8b823111 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.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" @@ -26,7 +26,7 @@ class MCP3008 : public Component, class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3008Sensor(MCP3008 *parent, const std::string &name, uint8_t pin, float reference_voltage); + MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage); void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } void setup() override; diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py index 4fc9b83afb..d4b9e979ce 100644 --- a/esphome/components/mcp3008/sensor.py +++ b/esphome/components/mcp3008/sensor.py @@ -1,7 +1,7 @@ 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_NAME +from esphome.const import CONF_ID, CONF_NUMBER from . import mcp3008_ns, MCP3008 AUTO_LOAD = ["voltage_sampler"] @@ -29,7 +29,6 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], parent, - config[CONF_NAME], config[CONF_NUMBER], config[CONF_REFERENCE_VOLTAGE], ) diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp index a8b130208e..2cb19282b6 100644 --- a/esphome/components/mcp4725/mcp4725.cpp +++ b/esphome/components/mcp4725/mcp4725.cpp @@ -8,13 +8,10 @@ static const char *const TAG = "mcp4725"; void MCP4725::setup() { ESP_LOGCONFIG(TAG, "Setting up MCP4725 (0x%02X)...", this->address_); - - this->raw_begin_transmission(); - - if (!this->raw_end_transmission()) { + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); - return; } } diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index 8f60df1d88..fca1331fc3 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -20,14 +20,14 @@ static const char *const TAG = "mcp9808"; void MCP9808Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up %s...", this->name_.c_str()); - uint16_t manu; - if (!this->read_byte_16(MCP9808_REG_MANUF_ID, &manu, 0) || manu != MCP9808_MANUF_ID) { + uint16_t manu = 0; + if (!this->read_byte_16(MCP9808_REG_MANUF_ID, &manu) || manu != MCP9808_MANUF_ID) { this->mark_failed(); ESP_LOGE(TAG, "%s manufacuturer id failed, device returned %X", this->name_.c_str(), manu); return; } - uint16_t dev_id; - if (!this->read_byte_16(MCP9808_REG_DEVICE_ID, &dev_id, 0) || dev_id != MCP9808_DEV_ID) { + uint16_t dev_id = 0; + if (!this->read_byte_16(MCP9808_REG_DEVICE_ID, &dev_id) || dev_id != MCP9808_DEV_ID) { this->mark_failed(); ESP_LOGE(TAG, "%s device id failed, device returned %X", this->name_.c_str(), dev_id); return; @@ -66,7 +66,7 @@ void MCP9808Sensor::update() { temp = (uint16_t)(msb) *16 + lsb / 16.0f; } - if (isnan(temp)) { + if (std::isnan(temp)) { this->status_set_warning(); return; } diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py index d417f45955..c7f6226e0b 100644 --- a/esphome/components/mcp9808/sensor.py +++ b/esphome/components/mcp9808/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ MCP9808Sensor = mcp9808_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py new file mode 100644 index 0000000000..b95469d9da --- /dev/null +++ b/esphome/components/mdns/__init__.py @@ -0,0 +1,45 @@ +from esphome.const import CONF_ID +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.core import CORE + +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["network"] + +mdns_ns = cg.esphome_ns.namespace("mdns") +MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) + + +def _remove_id_if_disabled(value): + value = value.copy() + if value[CONF_DISABLED]: + value.pop(CONF_ID) + return value + + +CONF_DISABLED = "disabled" +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MDNSComponent), + cv.Optional(CONF_DISABLED, default=False): cv.boolean, + } + ), + _remove_id_if_disabled, +) + + +async def to_code(config): + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("ESPmDNS", None) + elif CORE.is_esp8266: + cg.add_library("ESP8266mDNS", None) + + if config[CONF_DISABLED]: + return + + cg.add_define("USE_MDNS") + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp new file mode 100644 index 0000000000..372d980eb0 --- /dev/null +++ b/esphome/components/mdns/mdns_component.cpp @@ -0,0 +1,82 @@ +#include "mdns_component.h" +#include "esphome/core/defines.h" +#include "esphome/core/version.h" +#include "esphome/core/application.h" + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif +#ifdef USE_DASHBOARD_IMPORT +#include "esphome/components/dashboard_import/dashboard_import.h" +#endif + +namespace esphome { +namespace mdns { + +#ifndef WEBSERVER_PORT +#define WEBSERVER_PORT 80 // NOLINT +#endif + +std::vector MDNSComponent::compile_services_() { + std::vector res; + +#ifdef USE_API + if (api::global_api_server != nullptr) { + MDNSService service{}; + service.service_type = "esphomelib"; + service.proto = "_tcp"; + service.port = api::global_api_server->get_port(); + service.txt_records.push_back({"version", ESPHOME_VERSION}); + service.txt_records.push_back({"mac", get_mac_address()}); + const char *platform = nullptr; +#ifdef USE_ESP8266 + platform = "ESP8266"; +#endif +#ifdef USE_ESP32 + platform = "ESP32"; +#endif + if (platform != nullptr) { + service.txt_records.push_back({"platform", platform}); + } + + service.txt_records.push_back({"board", ESPHOME_BOARD}); + +#ifdef ESPHOME_PROJECT_NAME + service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); + service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); +#endif // ESPHOME_PROJECT_NAME + +#ifdef USE_DASHBOARD_IMPORT + service.txt_records.push_back({"package_import_url", dashboard_import::get_package_import_url()}); +#endif + + res.push_back(service); + } +#endif // USE_API + +#ifdef USE_PROMETHEUS + { + MDNSService service{}; + service.service_type = "prometheus-http"; + service.proto = "_tcp"; + service.port = WEBSERVER_PORT; + res.push_back(service); + } +#endif + + if (res.empty()) { + // Publish "http" service if not using native API + // This is just to have *some* mDNS service so that .local resolution works + MDNSService service{}; + service.service_type = "http"; + service.proto = "_tcp"; + service.port = WEBSERVER_PORT; + service.txt_records.push_back({"version", ESPHOME_VERSION}); + res.push_back(service); + } + return res; +} +std::string MDNSComponent::compile_hostname_() { return App.get_name(); } + +} // namespace mdns +} // namespace esphome diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h new file mode 100644 index 0000000000..985947d99c --- /dev/null +++ b/esphome/components/mdns/mdns_component.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" + +namespace esphome { +namespace mdns { + +struct MDNSTXTRecord { + std::string key; + std::string value; +}; + +struct MDNSService { + std::string service_type; + std::string proto; + uint16_t port; + std::vector txt_records; +}; + +class MDNSComponent : public Component { + public: + void setup() override; + +#if defined(USE_ESP8266) && defined(USE_ARDUINO) + void loop() override; +#endif + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + std::vector compile_services_(); + std::string compile_hostname_(); +}; + +} // namespace mdns +} // namespace esphome diff --git a/esphome/components/mdns/mdns_esp32_arduino.cpp b/esphome/components/mdns/mdns_esp32_arduino.cpp new file mode 100644 index 0000000000..4d13b7321a --- /dev/null +++ b/esphome/components/mdns/mdns_esp32_arduino.cpp @@ -0,0 +1,27 @@ +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "mdns_component.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace mdns { + +static const char *const TAG = "mdns"; + +void MDNSComponent::setup() { + MDNS.begin(compile_hostname_().c_str()); + + auto services = compile_services_(); + for (const auto &service : services) { + MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); + } + } +} + +} // namespace mdns +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp new file mode 100644 index 0000000000..48f31f1bbf --- /dev/null +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -0,0 +1,32 @@ +#if defined(USE_ESP8266) && defined(USE_ARDUINO) + +#include "mdns_component.h" +#include "esphome/core/log.h" +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include + +namespace esphome { +namespace mdns { + +static const char *const TAG = "mdns"; + +void MDNSComponent::setup() { + network::IPAddress addr = network::get_ip_address(); + MDNS.begin(compile_hostname_().c_str(), (uint32_t) addr); + + auto services = compile_services_(); + for (const auto &service : services) { + MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); + } + } +} + +void MDNSComponent::loop() { MDNS.update(); } + +} // namespace mdns +} // namespace esphome + +#endif diff --git a/esphome/components/mdns/mdns_esp_idf.cpp b/esphome/components/mdns/mdns_esp_idf.cpp new file mode 100644 index 0000000000..17874f1ffe --- /dev/null +++ b/esphome/components/mdns/mdns_esp_idf.cpp @@ -0,0 +1,52 @@ +#ifdef USE_ESP_IDF + +#include "mdns_component.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace mdns { + +static const char *const TAG = "mdns"; + +void MDNSComponent::setup() { + esp_err_t err = mdns_init(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "MDNS init failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + mdns_hostname_set(compile_hostname_().c_str()); + mdns_instance_name_set(compile_hostname_().c_str()); + + auto services = compile_services_(); + for (const auto &service : services) { + std::vector txt_records; + for (const auto &record : service.txt_records) { + mdns_txt_item_t it{}; + // dup strings to ensure the pointer is valid even after the record loop + it.key = strdup(record.key.c_str()); + it.value = strdup(record.value.c_str()); + txt_records.push_back(it); + } + err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), service.port, + txt_records.data(), txt_records.size()); + + // free records + for (const auto &it : txt_records) { + delete it.key; // NOLINT(cppcoreguidelines-owning-memory) + delete it.value; // NOLINT(cppcoreguidelines-owning-memory) + } + + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to register mDNS service %s: %s", service.service_type.c_str(), esp_err_to_name(err)); + } + } +} + +} // namespace mdns +} // namespace esphome + +#endif diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index ebcecb84e2..0081f42952 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -7,13 +7,12 @@ from esphome.const import ( CONF_CO2, CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, - ICON_EMPTY, ) DEPENDENCIES = ["uart"] @@ -33,18 +32,17 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MHZ19Component), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, } diff --git a/esphome/components/midea/__init__.py b/esphome/components/midea/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/adapter.cpp new file mode 100644 index 0000000000..a3f19dbda8 --- /dev/null +++ b/esphome/components/midea/adapter.cpp @@ -0,0 +1,177 @@ +#ifdef USE_ARDUINO + +#include "esphome/core/log.h" +#include "adapter.h" + +namespace esphome { +namespace midea { + +const char *const Constants::TAG = "midea"; +const std::string Constants::FREEZE_PROTECTION = "freeze protection"; +const std::string Constants::SILENT = "silent"; +const std::string Constants::TURBO = "turbo"; + +ClimateMode Converters::to_climate_mode(MideaMode mode) { + switch (mode) { + case MideaMode::MODE_AUTO: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + case MideaMode::MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MideaMode::MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MideaMode::MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MideaMode::MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_OFF; + } +} + +MideaMode Converters::to_midea_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_HEAT_COOL: + return MideaMode::MODE_AUTO; + case ClimateMode::CLIMATE_MODE_COOL: + return MideaMode::MODE_COOL; + case ClimateMode::CLIMATE_MODE_DRY: + return MideaMode::MODE_DRY; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + return MideaMode::MODE_FAN_ONLY; + case ClimateMode::CLIMATE_MODE_HEAT: + return MideaMode::MODE_HEAT; + default: + return MideaMode::MODE_OFF; + } +} + +ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) { + switch (mode) { + case MideaSwingMode::SWING_VERTICAL: + return ClimateSwingMode::CLIMATE_SWING_VERTICAL; + case MideaSwingMode::SWING_HORIZONTAL: + return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL; + case MideaSwingMode::SWING_BOTH: + return ClimateSwingMode::CLIMATE_SWING_BOTH; + default: + return ClimateSwingMode::CLIMATE_SWING_OFF; + } +} + +MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) { + switch (mode) { + case ClimateSwingMode::CLIMATE_SWING_VERTICAL: + return MideaSwingMode::SWING_VERTICAL; + case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: + return MideaSwingMode::SWING_HORIZONTAL; + case ClimateSwingMode::CLIMATE_SWING_BOTH: + return MideaSwingMode::SWING_BOTH; + default: + return MideaSwingMode::SWING_OFF; + } +} + +MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + return MideaFanMode::FAN_LOW; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + return MideaFanMode::FAN_MEDIUM; + case ClimateFanMode::CLIMATE_FAN_HIGH: + return MideaFanMode::FAN_HIGH; + default: + return MideaFanMode::FAN_AUTO; + } +} + +ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case MideaFanMode::FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case MideaFanMode::FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + case MideaFanMode::FAN_TURBO: + return true; + default: + return false; + } +} + +const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + return Constants::SILENT; + default: + return Constants::TURBO; + } +} + +MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { + if (mode == Constants::SILENT) + return MideaFanMode::FAN_SILENT; + return MideaFanMode::FAN_TURBO; +} + +MideaPreset Converters::to_midea_preset(ClimatePreset preset) { + switch (preset) { + case ClimatePreset::CLIMATE_PRESET_SLEEP: + return MideaPreset::PRESET_SLEEP; + case ClimatePreset::CLIMATE_PRESET_ECO: + return MideaPreset::PRESET_ECO; + case ClimatePreset::CLIMATE_PRESET_BOOST: + return MideaPreset::PRESET_TURBO; + default: + return MideaPreset::PRESET_NONE; + } +} + +ClimatePreset Converters::to_climate_preset(MideaPreset preset) { + switch (preset) { + case MideaPreset::PRESET_SLEEP: + return ClimatePreset::CLIMATE_PRESET_SLEEP; + case MideaPreset::PRESET_ECO: + return ClimatePreset::CLIMATE_PRESET_ECO; + case MideaPreset::PRESET_TURBO: + return ClimatePreset::CLIMATE_PRESET_BOOST; + default: + return ClimatePreset::CLIMATE_PRESET_NONE; + } +} + +bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } + +const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } + +MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } + +void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { + if (capabilities.supportAutoMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL); + if (capabilities.supportCoolMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL); + if (capabilities.supportHeatMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT); + if (capabilities.supportDryMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY); + if (capabilities.supportTurboPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); + if (capabilities.supportEcoPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); + if (capabilities.supportFrostProtectionPreset()) + traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); +} + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/adapter.h new file mode 100644 index 0000000000..2497cbbe5b --- /dev/null +++ b/esphome/components/midea/adapter.h @@ -0,0 +1,47 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include +#include "esphome/components/climate/climate_traits.h" +#include "appliance_base.h" + +namespace esphome { +namespace midea { + +using MideaMode = dudanov::midea::ac::Mode; +using MideaSwingMode = dudanov::midea::ac::SwingMode; +using MideaFanMode = dudanov::midea::ac::FanMode; +using MideaPreset = dudanov::midea::ac::Preset; + +class Constants { + public: + static const char *const TAG; + static const std::string FREEZE_PROTECTION; + static const std::string SILENT; + static const std::string TURBO; +}; + +class Converters { + public: + static MideaMode to_midea_mode(ClimateMode mode); + static ClimateMode to_climate_mode(MideaMode mode); + static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); + static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); + static MideaPreset to_midea_preset(ClimatePreset preset); + static MideaPreset to_midea_preset(const std::string &preset); + static bool is_custom_midea_preset(MideaPreset preset); + static ClimatePreset to_climate_preset(MideaPreset preset); + static const std::string &to_custom_climate_preset(MideaPreset preset); + static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); + static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); + static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); + static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); +}; + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp new file mode 100644 index 0000000000..103b852936 --- /dev/null +++ b/esphome/components/midea/air_conditioner.cpp @@ -0,0 +1,156 @@ +#ifdef USE_ARDUINO + +#include "esphome/core/log.h" +#include "air_conditioner.h" +#include "adapter.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "midea_ir.h" +#endif + +namespace esphome { +namespace midea { + +static void set_sensor(Sensor *sensor, float value) { + if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) + sensor->publish_state(value); +} + +template void update_property(T &property, const T &value, bool &flag) { + if (property != value) { + property = value; + flag = true; + } +} + +void AirConditioner::on_status_change() { + bool need_publish = false; + update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish); + update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish); + auto mode = Converters::to_climate_mode(this->base_.getMode()); + update_property(this->mode, mode, need_publish); + auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode()); + update_property(this->swing_mode, swing_mode, need_publish); + // Preset + auto preset = this->base_.getPreset(); + if (Converters::is_custom_midea_preset(preset)) { + if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset))) + need_publish = true; + } else if (this->set_preset_(Converters::to_climate_preset(preset))) { + need_publish = true; + } + // Fan mode + auto fan_mode = this->base_.getFanMode(); + if (Converters::is_custom_midea_fan_mode(fan_mode)) { + if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode))) + need_publish = true; + } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) { + need_publish = true; + } + if (need_publish) + this->publish_state(); + set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp()); + set_sensor(this->power_sensor_, this->base_.getPowerUsage()); + set_sensor(this->humidity_sensor_, this->base_.getIndoorHum()); +} + +void AirConditioner::control(const ClimateCall &call) { + dudanov::midea::ac::Control ctrl{}; + if (call.get_target_temperature().has_value()) + ctrl.targetTemp = call.get_target_temperature().value(); + if (call.get_swing_mode().has_value()) + ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); + if (call.get_mode().has_value()) + ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); + if (call.get_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); + else if (call.get_custom_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + if (call.get_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); + else if (call.get_custom_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + this->base_.control(ctrl); +} + +ClimateTraits AirConditioner::traits() { + auto traits = ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_visual_min_temperature(17); + traits.set_visual_max_temperature(30); + traits.set_visual_temperature_step(0.5); + traits.set_supported_modes(this->supported_modes_); + traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_supported_presets(this->supported_presets_); + traits.set_supported_custom_presets(this->supported_custom_presets_); + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + /* + MINIMAL SET OF CAPABILITIES */ + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) + Converters::to_climate_traits(traits, this->base_.getCapabilities()); + return traits; +} + +void AirConditioner::dump_config() { + ESP_LOGCONFIG(Constants::TAG, "MideaDongle:"); + ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod()); + ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout()); + ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts()); +#ifdef USE_REMOTE_TRANSMITTER + ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter"); +#endif + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { + this->base_.getCapabilities().dump(); + } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) { + ESP_LOGW(Constants::TAG, + "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your " + "appliance options."); + } + this->dump_traits_(Constants::TAG); +} + +/* ACTIONS */ + +void AirConditioner::do_follow_me(float temperature, bool beeper) { +#ifdef USE_REMOTE_TRANSMITTER + IrFollowMeData data(static_cast(lroundf(temperature)), beeper); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_swing_step() { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x01); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_display_toggle() { + if (this->base_.getCapabilities().supportLightControl()) { + this->base_.displayToggle(); + } else { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x08); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif + } +} + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h new file mode 100644 index 0000000000..8dfb9dcb3d --- /dev/null +++ b/esphome/components/midea/air_conditioner.h @@ -0,0 +1,46 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include +#include "appliance_base.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace midea { + +using sensor::Sensor; +using climate::ClimateCall; + +class AirConditioner : public ApplianceBase { + public: + void dump_config() override; + void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } + void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; } + void on_status_change() override; + + /* ############### */ + /* ### ACTIONS ### */ + /* ############### */ + + void do_follow_me(float temperature, bool beeper = false); + void do_display_toggle(); + void do_swing_step(); + void do_beeper_on() { this->set_beeper_feedback(true); } + void do_beeper_off() { this->set_beeper_feedback(false); } + void do_power_on() { this->base_.setPowerState(true); } + void do_power_off() { this->base_.setPowerState(false); } + + protected: + void control(const ClimateCall &call) override; + ClimateTraits traits() override; + Sensor *outdoor_sensor_{nullptr}; + Sensor *humidity_sensor_{nullptr}; + Sensor *power_sensor_{nullptr}; +}; + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h new file mode 100644 index 0000000000..88a722e389 --- /dev/null +++ b/esphome/components/midea/appliance_base.h @@ -0,0 +1,89 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#endif +#include +#include + +namespace esphome { +namespace midea { + +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +template +class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate, public Stream { + static_assert(std::is_base_of::value, + "T must derive from dudanov::midea::ApplianceBase class"); + + public: + ApplianceBase() { + this->base_.setStream(this); + this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + dudanov::midea::ApplianceBase::setLogger( + [](int level, const char *tag, int line, const String &format, va_list args) { + esp_log_vprintf_(level, tag, line, format.c_str(), args); + }); + } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + void set_period(uint32_t ms) { this->base_.setPeriod(ms); } + void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } + void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } + void set_autoconf(bool value) { this->base_.setAutoconf(value); } + void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } + void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } + void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } + void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } + virtual void on_status_change() = 0; +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void transmit_ir(remote_base::MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } +#endif + + int available() override { return uart::UARTDevice::available(); } + int read() override { return uart::UARTDevice::read(); } + int peek() override { return uart::UARTDevice::peek(); } + void flush() override { uart::UARTDevice::flush(); } + size_t write(uint8_t data) override { return uart::UARTDevice::write(data); } + + protected: + T base_; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; +#ifdef USE_REMOTE_TRANSMITTER + remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; +#endif +}; + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/automations.h new file mode 100644 index 0000000000..5b638286ac --- /dev/null +++ b/esphome/components/midea/automations.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/automation.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { + +template class MideaActionBase : public Action { + public: + void set_parent(AirConditioner *parent) { this->parent_ = parent; } + + protected: + AirConditioner *parent_; +}; + +template class FollowMeAction : public MideaActionBase { + TEMPLATABLE_VALUE(float, temperature) + TEMPLATABLE_VALUE(bool, beeper) + + void play(Ts... x) override { + this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); + } +}; + +template class SwingStepAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_swing_step(); } +}; + +template class DisplayToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_display_toggle(); } +}; + +template class BeeperOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_on(); } +}; + +template class BeeperOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_off(); } +}; + +template class PowerOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_on(); } +}; + +template class PowerOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_off(); } +}; + +} // namespace midea +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py new file mode 100644 index 0000000000..08e82025b6 --- /dev/null +++ b/esphome/components/midea/climate.py @@ -0,0 +1,285 @@ +from esphome.core import coroutine +from esphome import automation +from esphome.components import climate, sensor, uart, remote_transmitter +from esphome.components.remote_base import CONF_TRANSMITTER_ID +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_AUTOCONF, + CONF_BEEPER, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESETS, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_PERIOD, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TIMEOUT, + CONF_TEMPERATURE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + ICON_POWER, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_WATT, +) +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) + +CODEOWNERS = ["@dudanov"] +DEPENDENCIES = ["climate", "uart", "wifi"] +AUTO_LOAD = ["sensor"] +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_POWER_USAGE = "power_usage" +CONF_HUMIDITY_SETPOINT = "humidity_setpoint" +midea_ns = cg.esphome_ns.namespace("midea") +AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ns.namespace("Constants") + + +def templatize(value): + if isinstance(value, cv.Schema): + value = value.schema + ret = {} + for key, val in value.items(): + ret[key] = cv.templatable(val) + return cv.Schema(ret) + + +def register_action(name, type_, schema): + validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) + registerer = automation.register_action(f"midea_ac.{name}", type_, validator) + + def decorator(func): + async def new_func(config, action_id, template_arg, args): + ac_ = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(ac_)) + await coroutine(func)(var, config, args) + return var + + return registerer(new_func) + + return decorator + + +ALLOWED_CLIMATE_MODES = { + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +ALLOWED_CLIMATE_PRESETS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + +ALLOWED_CLIMATE_SWING_MODES = { + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +CUSTOM_FAN_MODES = { + "SILENT": Capabilities.SILENT, + "TURBO": Capabilities.TURBO, +} + +CUSTOM_PRESETS = { + "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION, +} + +validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True) +validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True) +validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) +validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirConditioner), + cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, + cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, + cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), + cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + cv.Optional(CONF_BEEPER, default=False): cv.boolean, + cv.Optional(CONF_AUTOCONF, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( + validate_swing_modes + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets), + cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( + validate_custom_fan_modes + ), + cv.Optional(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, + ), + cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY_SETPOINT): 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, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) + +# Actions +FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) + +MIDEA_ACTION_BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(AirConditioner), + } +) + +# FollowMe action +MIDEA_FOLLOW_ME_MIN = 0 +MIDEA_FOLLOW_ME_MAX = 37 +MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), + } +) + + +@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA) +async def follow_me_to_code(var, config, args): + template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) + cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) + cg.add(var.set_temperature(template_)) + + +# Toggle Display action +@register_action( + "display_toggle", + DisplayToggleAction, + cv.Schema({}), +) +async def display_toggle_to_code(var, config, args): + pass + + +# Swing Step action +@register_action( + "swing_step", + SwingStepAction, + cv.Schema({}), +) +async def swing_step_to_code(var, config, args): + pass + + +# Beeper On action +@register_action( + "beeper_on", + BeeperOnAction, + cv.Schema({}), +) +async def beeper_on_to_code(var, config, args): + pass + + +# Beeper Off action +@register_action( + "beeper_off", + BeeperOffAction, + cv.Schema({}), +) +async def beeper_off_to_code(var, config, args): + pass + + +# Power On action +@register_action( + "power_on", + PowerOnAction, + cv.Schema({}), +) +async def power_on_to_code(var, config, args): + pass + + +# Power Off action +@register_action( + "power_off", + PowerOffAction, + cv.Schema({}), +) +async def power_off_to_code(var, config, args): + pass + + +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) + await climate.register_climate(var, config) + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) + cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) + cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) + if CONF_TRANSMITTER_ID in config: + cg.add_define("USE_REMOTE_TRANSMITTER") + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) + cg.add(var.set_autoconf(config[CONF_AUTOCONF])) + 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_CUSTOM_PRESETS in config: + cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) + if CONF_CUSTOM_FAN_MODES in config: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_POWER_USAGE in config: + sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) + cg.add(var.set_power_sensor(sens)) + 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") diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/midea_ir.h new file mode 100644 index 0000000000..abd4324bcc --- /dev/null +++ b/esphome/components/midea/midea_ir.h @@ -0,0 +1,45 @@ +#pragma once + +#ifdef USE_ARDUINO +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" + +namespace esphome { +namespace midea { + +using IrData = remote_base::MideaData; + +class IrFollowMeData : public IrData { + public: + // Default constructor (temp: 30C, beeper: off) + IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + IrFollowMeData(const IrData &data) : IrData(data) {} + // Direct from temperature and beeper values + IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->data_[4] - 1; } + void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } + + /* BEEPER */ + bool beeper() const { return this->data_[3] & 128; } + void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class IrSpecialData : public IrData { + public: + IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} +}; + +} // namespace midea +} // namespace esphome + +#endif +#endif // USE_ARDUINO diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 00aa979515..f336f84787 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -1,111 +1,3 @@ -from esphome.components import climate, sensor import esphome.config_validation as cv -import esphome.codegen as cg -from esphome.const import ( - CONF_CUSTOM_FAN_MODES, - CONF_CUSTOM_PRESETS, - CONF_ID, - CONF_PRESET_BOOST, - CONF_PRESET_ECO, - CONF_PRESET_SLEEP, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_PERCENT, - UNIT_WATT, - ICON_THERMOMETER, - ICON_POWER, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ICON_WATER_PERCENT, - DEVICE_CLASS_HUMIDITY, -) -from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle -AUTO_LOAD = ["climate", "sensor", "midea_dongle"] -CODEOWNERS = ["@dudanov"] -CONF_BEEPER = "beeper" -CONF_SWING_HORIZONTAL = "swing_horizontal" -CONF_SWING_BOTH = "swing_both" -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" -CONF_POWER_USAGE = "power_usage" -CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ac_ns = cg.esphome_ns.namespace("midea_ac") -MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component) - -CLIMATE_CUSTOM_FAN_MODES = { - "SILENT": "silent", - "TURBO": "turbo", -} - -validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True) - -CLIMATE_CUSTOM_PRESETS = { - "FREEZE_PROTECTION": "freeze protection", -} - -validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True) - -CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MideaAC), - cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle), - cv.Optional(CONF_BEEPER, default=False): cv.boolean, - cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( - validate_climate_custom_fan_mode - ), - cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list( - validate_climate_custom_preset - ), - cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean, - cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean, - cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean, - cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, - cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, - cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( - UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT - ), - cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, - ), - } - ).extend(cv.COMPONENT_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) - paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID]) - cg.add(var.set_midea_dongle_parent(paren)) - cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) - if CONF_CUSTOM_FAN_MODES in config: - cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - if CONF_CUSTOM_PRESETS in config: - cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) - cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL])) - cg.add(var.set_swing_both(config[CONF_SWING_BOTH])) - cg.add(var.set_preset_eco(config[CONF_PRESET_ECO])) - cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP])) - cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST])) - if CONF_OUTDOOR_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) - cg.add(var.set_outdoor_temperature_sensor(sens)) - if CONF_POWER_USAGE in config: - sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) - cg.add(var.set_power_sensor(sens)) - if CONF_HUMIDITY_SETPOINT in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) - cg.add(var.set_humidity_setpoint_sensor(sens)) +CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp deleted file mode 100644 index 1efe6c93f8..0000000000 --- a/esphome/components/midea_ac/midea_climate.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "esphome/core/log.h" -#include "midea_climate.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; - -static void set_sensor(sensor::Sensor *sensor, float value) { - if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) - sensor->publish_state(value); -} - -template void set_property(T &property, T value, bool &flag) { - if (property != value) { - property = value; - flag = true; - } -} - -void MideaAC::on_frame(const midea_dongle::Frame &frame) { - const auto p = frame.as(); - if (p.has_power_info()) { - set_sensor(this->power_sensor_, p.get_power_usage()); - return; - } else if (!p.has_properties()) { - ESP_LOGW(TAG, "RX: frame has unknown type"); - return; - } - if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) { - ESP_LOGD(TAG, "RX: control frame"); - this->ctrl_request_ = false; - } else { - ESP_LOGD(TAG, "RX: query frame"); - } - if (this->ctrl_request_) - return; - this->cmd_frame_.set_properties(p); // copy properties from response - bool need_publish = false; - set_property(this->mode, p.get_mode(), need_publish); - set_property(this->target_temperature, p.get_target_temp(), need_publish); - set_property(this->current_temperature, p.get_indoor_temp(), need_publish); - if (p.is_custom_fan_mode()) { - this->fan_mode.reset(); - optional mode = p.get_custom_fan_mode(); - set_property(this->custom_fan_mode, mode, need_publish); - } else { - this->custom_fan_mode.reset(); - optional mode = p.get_fan_mode(); - set_property(this->fan_mode, mode, need_publish); - } - set_property(this->swing_mode, p.get_swing_mode(), need_publish); - if (p.is_custom_preset()) { - this->preset.reset(); - optional preset = p.get_custom_preset(); - set_property(this->custom_preset, preset, need_publish); - } else { - this->custom_preset.reset(); - set_property(this->preset, p.get_preset(), need_publish); - } - if (need_publish) - this->publish_state(); - set_sensor(this->outdoor_sensor_, p.get_outdoor_temp()); - set_sensor(this->humidity_sensor_, p.get_humidity_setpoint()); -} - -void MideaAC::on_update() { - if (this->ctrl_request_) { - ESP_LOGD(TAG, "TX: control"); - this->parent_->write_frame(this->cmd_frame_); - } else { - ESP_LOGD(TAG, "TX: query"); - if (this->power_sensor_ == nullptr || this->request_num_++ % 32) - this->parent_->write_frame(this->query_frame_); - else - this->parent_->write_frame(this->power_frame_); - } -} - -bool MideaAC::allow_preset(climate::ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - if (this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "ECO preset is only available in COOL mode"); - } - break; - case climate::CLIMATE_PRESET_SLEEP: - if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) { - ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode"); - } else { - return true; - } - break; - case climate::CLIMATE_PRESET_BOOST: - if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); - } - break; - case climate::CLIMATE_PRESET_HOME: - return true; - default: - break; - } - return false; -} - -bool MideaAC::allow_custom_preset(const std::string &custom_preset) const { - if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) { - if (this->mode == climate::CLIMATE_MODE_HEAT) { - return true; - } else { - ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str()); - } - } - return false; -} - -void MideaAC::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value() && call.get_mode().value() != this->mode) { - this->cmd_frame_.set_mode(call.get_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) { - this->cmd_frame_.set_target_temp(call.get_target_temperature().value()); - this->ctrl_request_ = true; - } - if (call.get_fan_mode().has_value() && - (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) { - this->custom_fan_mode.reset(); - this->cmd_frame_.set_fan_mode(call.get_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_fan_mode().has_value() && - (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) { - this->fan_mode.reset(); - this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) { - this->cmd_frame_.set_swing_mode(call.get_swing_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) && - (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) { - this->custom_preset.reset(); - this->cmd_frame_.set_preset(call.get_preset().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) && - (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) { - this->preset.reset(); - this->cmd_frame_.set_custom_preset(call.get_custom_preset().value()); - this->ctrl_request_ = true; - } - if (this->ctrl_request_) { - this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_); - this->cmd_frame_.finalize(); - } -} - -climate::ClimateTraits MideaAC::traits() { - auto traits = climate::ClimateTraits(); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(0.5); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(true); - traits.set_supports_dry_mode(true); - traits.set_supports_heat_mode(true); - traits.set_supports_fan_only_mode(true); - traits.set_supports_fan_mode_auto(true); - traits.set_supports_fan_mode_low(true); - traits.set_supports_fan_mode_medium(true); - traits.set_supports_fan_mode_high(true); - traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); - traits.set_supports_swing_mode_off(true); - traits.set_supports_swing_mode_vertical(true); - traits.set_supports_swing_mode_horizontal(this->traits_swing_horizontal_); - traits.set_supports_swing_mode_both(this->traits_swing_both_); - traits.set_supports_preset_home(true); - traits.set_supports_preset_eco(this->traits_preset_eco_); - traits.set_supports_preset_sleep(this->traits_preset_sleep_); - traits.set_supports_preset_boost(this->traits_preset_boost_); - traits.set_supported_custom_presets(this->traits_custom_presets_); - traits.set_supports_current_temperature(true); - return traits; -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h deleted file mode 100644 index 1d21c4cab2..0000000000 --- a/esphome/components/midea_ac/midea_climate.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/climate/climate.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component { - public: - float get_setup_priority() const override { return setup_priority::LATE; } - void on_frame(const midea_dongle::Frame &frame) override; - void on_update() override; - void setup() override { this->parent_->set_appliance(this); } - void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; } - void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } - void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } - void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } - void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; } - void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; } - void set_swing_both(bool state) { this->traits_swing_both_ = state; } - void set_preset_eco(bool state) { this->traits_preset_eco_ = state; } - void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } - void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } - bool allow_preset(climate::ClimatePreset preset) const; - void set_custom_fan_modes(std::vector custom_fan_modes) { - this->traits_custom_fan_modes_ = std::move(custom_fan_modes); - } - void set_custom_presets(std::vector custom_presets) { - this->traits_custom_presets_ = std::move(custom_presets); - } - bool allow_custom_preset(const std::string &custom_preset) const; - - protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - - const QueryFrame query_frame_; - const PowerQueryFrame power_frame_; - CommandFrame cmd_frame_; - midea_dongle::MideaDongle *parent_{nullptr}; - sensor::Sensor *outdoor_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *power_sensor_{nullptr}; - uint8_t request_num_{0}; - bool ctrl_request_{false}; - bool beeper_feedback_{false}; - bool traits_swing_horizontal_{false}; - bool traits_swing_both_{false}; - bool traits_preset_eco_{false}; - bool traits_preset_sleep_{false}; - bool traits_preset_boost_{false}; - std::vector traits_custom_fan_modes_{{}}; - std::vector traits_custom_presets_{{}}; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp deleted file mode 100644 index 3d210d89f0..0000000000 --- a/esphome/components/midea_ac/midea_frame.cpp +++ /dev/null @@ -1,232 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; -const std::string MIDEA_SILENT_FAN_MODE = "silent"; -const std::string MIDEA_TURBO_FAN_MODE = "turbo"; -const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; - -const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00, - 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68}; - -const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, - 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A}; - -const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00, - 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -float PropertiesFrame::get_target_temp() const { - float temp = static_cast((this->pbuf_[12] & 0x0F) + 16); - if (this->pbuf_[12] & 0x10) - temp += 0.5; - return temp; -} - -void PropertiesFrame::set_target_temp(float temp) { - uint8_t tmp = static_cast(temp * 16.0) + 4; - tmp = ((tmp & 8) << 1) | (tmp >> 4); - this->pbuf_[12] &= ~0x1F; - this->pbuf_[12] |= tmp; -} - -static float i16tof(int16_t in) { return static_cast(in - 50) / 2.0; } -float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); } -float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); } -float PropertiesFrame::get_humidity_setpoint() const { return static_cast(this->pbuf_[29] & 0x7F); } - -climate::ClimateMode PropertiesFrame::get_mode() const { - if (!this->get_power_()) - return climate::CLIMATE_MODE_OFF; - switch (this->pbuf_[12] >> 5) { - case MIDEA_MODE_AUTO: - return climate::CLIMATE_MODE_AUTO; - case MIDEA_MODE_COOL: - return climate::CLIMATE_MODE_COOL; - case MIDEA_MODE_DRY: - return climate::CLIMATE_MODE_DRY; - case MIDEA_MODE_HEAT: - return climate::CLIMATE_MODE_HEAT; - case MIDEA_MODE_FAN_ONLY: - return climate::CLIMATE_MODE_FAN_ONLY; - default: - return climate::CLIMATE_MODE_OFF; - } -} - -void PropertiesFrame::set_mode(climate::ClimateMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_MODE_AUTO: - m = MIDEA_MODE_AUTO; - break; - case climate::CLIMATE_MODE_COOL: - m = MIDEA_MODE_COOL; - break; - case climate::CLIMATE_MODE_DRY: - m = MIDEA_MODE_DRY; - break; - case climate::CLIMATE_MODE_HEAT: - m = MIDEA_MODE_HEAT; - break; - case climate::CLIMATE_MODE_FAN_ONLY: - m = MIDEA_MODE_FAN_ONLY; - break; - default: - this->set_power_(false); - return; - } - this->set_power_(true); - this->pbuf_[12] &= ~0xE0; - this->pbuf_[12] |= m << 5; -} - -optional PropertiesFrame::get_preset() const { - if (this->get_eco_mode()) { - return climate::CLIMATE_PRESET_ECO; - } else if (this->get_sleep_mode()) { - return climate::CLIMATE_PRESET_SLEEP; - } else if (this->get_turbo_mode()) { - return climate::CLIMATE_PRESET_BOOST; - } else { - return climate::CLIMATE_PRESET_HOME; - } -} - -void PropertiesFrame::set_preset(climate::ClimatePreset preset) { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - this->set_eco_mode(true); - break; - case climate::CLIMATE_PRESET_SLEEP: - this->set_sleep_mode(true); - break; - case climate::CLIMATE_PRESET_BOOST: - this->set_turbo_mode(true); - break; - default: - break; - } -} - -bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } - -const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; - -void PropertiesFrame::set_custom_preset(const std::string &preset) { - if (preset == MIDEA_FREEZE_PROTECTION_PRESET) { - this->set_freeze_protection_mode(true); - } -} - -bool PropertiesFrame::is_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - case MIDEA_FAN_TURBO: - return true; - default: - return false; - } -} - -climate::ClimateFanMode PropertiesFrame::get_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_LOW: - return climate::CLIMATE_FAN_LOW; - case MIDEA_FAN_MEDIUM: - return climate::CLIMATE_FAN_MEDIUM; - case MIDEA_FAN_HIGH: - return climate::CLIMATE_FAN_HIGH; - default: - return climate::CLIMATE_FAN_AUTO; - } -} - -void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_FAN_LOW: - m = MIDEA_FAN_LOW; - break; - case climate::CLIMATE_FAN_MEDIUM: - m = MIDEA_FAN_MEDIUM; - break; - case climate::CLIMATE_FAN_HIGH: - m = MIDEA_FAN_HIGH; - break; - default: - m = MIDEA_FAN_AUTO; - break; - } - this->pbuf_[13] = m; -} - -const std::string &PropertiesFrame::get_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - return MIDEA_SILENT_FAN_MODE; - default: - return MIDEA_TURBO_FAN_MODE; - } -} - -void PropertiesFrame::set_custom_fan_mode(const std::string &mode) { - uint8_t m; - if (mode == MIDEA_SILENT_FAN_MODE) { - m = MIDEA_FAN_SILENT; - } else { - m = MIDEA_FAN_TURBO; - } - this->pbuf_[13] = m; -} - -climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const { - switch (this->pbuf_[17] & 0x0F) { - case MIDEA_SWING_VERTICAL: - return climate::CLIMATE_SWING_VERTICAL; - case MIDEA_SWING_HORIZONTAL: - return climate::CLIMATE_SWING_HORIZONTAL; - case MIDEA_SWING_BOTH: - return climate::CLIMATE_SWING_BOTH; - default: - return climate::CLIMATE_SWING_OFF; - } -} - -void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_SWING_VERTICAL: - m = MIDEA_SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - m = MIDEA_SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - m = MIDEA_SWING_BOTH; - break; - default: - m = MIDEA_SWING_OFF; - break; - } - this->pbuf_[17] = 0x30 | m; -} - -float PropertiesFrame::get_power_usage() const { - uint32_t power = 0; - const uint8_t *ptr = this->pbuf_ + 28; - for (uint32_t weight = 1;; weight *= 10, ptr--) { - power += (*ptr % 16) * weight; - weight *= 10; - power += (*ptr / 16) * weight; - if (weight == 100000) - return static_cast(power) * 0.1; - } -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h deleted file mode 100644 index a84161b4af..0000000000 --- a/esphome/components/midea_ac/midea_frame.h +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_frame.h" - -namespace esphome { -namespace midea_ac { - -extern const std::string MIDEA_SILENT_FAN_MODE; -extern const std::string MIDEA_TURBO_FAN_MODE; -extern const std::string MIDEA_FREEZE_PROTECTION_PRESET; - -/// Enum for all modes a Midea device can be in. -enum MideaMode : uint8_t { - /// The Midea device is set to automatically change the heating/cooling cycle - MIDEA_MODE_AUTO = 1, - /// The Midea device is manually set to cool mode (not in auto mode!) - MIDEA_MODE_COOL = 2, - /// The Midea device is manually set to dry mode - MIDEA_MODE_DRY = 3, - /// The Midea device is manually set to heat mode (not in auto mode!) - MIDEA_MODE_HEAT = 4, - /// The Midea device is manually set to fan only mode - MIDEA_MODE_FAN_ONLY = 5, -}; - -/// Enum for all modes a Midea fan can be in -enum MideaFanMode : uint8_t { - /// The fan mode is set to Auto - MIDEA_FAN_AUTO = 102, - /// The fan mode is set to Silent - MIDEA_FAN_SILENT = 20, - /// The fan mode is set to Low - MIDEA_FAN_LOW = 40, - /// The fan mode is set to Medium - MIDEA_FAN_MEDIUM = 60, - /// The fan mode is set to High - MIDEA_FAN_HIGH = 80, - /// The fan mode is set to Turbo - MIDEA_FAN_TURBO = 100, -}; - -/// Enum for all modes a Midea swing can be in -enum MideaSwingMode : uint8_t { - /// The sing mode is set to Off - MIDEA_SWING_OFF = 0b0000, - /// The fan mode is set to Both - MIDEA_SWING_BOTH = 0b1111, - /// The fan mode is set to Vertical - MIDEA_SWING_VERTICAL = 0b1100, - /// The fan mode is set to Horizontal - MIDEA_SWING_HORIZONTAL = 0b0011, -}; - -class PropertiesFrame : public midea_dongle::BaseFrame { - public: - PropertiesFrame() = delete; - PropertiesFrame(uint8_t *data) : BaseFrame(data) {} - PropertiesFrame(const Frame &frame) : BaseFrame(frame) {} - - bool has_properties() const { - return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02)); - } - - bool has_power_info() const { return this->has_response_type(0xC1); } - - /* TARGET TEMPERATURE */ - - float get_target_temp() const; - void set_target_temp(float temp); - - /* MODE */ - climate::ClimateMode get_mode() const; - void set_mode(climate::ClimateMode mode); - - /* FAN SPEED */ - bool is_custom_fan_mode() const; - climate::ClimateFanMode get_fan_mode() const; - void set_fan_mode(climate::ClimateFanMode mode); - - const std::string &get_custom_fan_mode() const; - void set_custom_fan_mode(const std::string &mode); - - /* SWING MODE */ - climate::ClimateSwingMode get_swing_mode() const; - void set_swing_mode(climate::ClimateSwingMode mode); - - /* INDOOR TEMPERATURE */ - float get_indoor_temp() const; - - /* OUTDOOR TEMPERATURE */ - float get_outdoor_temp() const; - - /* HUMIDITY SETPOINT */ - float get_humidity_setpoint() const; - - /* ECO MODE */ - bool get_eco_mode() const { return this->pbuf_[19] & 0x10; } - void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); } - - /* SLEEP MODE */ - bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; } - void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } - - /* TURBO MODE */ - bool get_turbo_mode() const { return this->pbuf_[18] & 0x20; } - void set_turbo_mode(bool state) { this->set_bytemask_(18, 0x20, state); } - - /* FREEZE PROTECTION */ - bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } - void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); } - - /* PRESET */ - optional get_preset() const; - void set_preset(climate::ClimatePreset preset); - - bool is_custom_preset() const; - const std::string &get_custom_preset() const; - void set_custom_preset(const std::string &preset); - - /* POWER USAGE */ - float get_power_usage() const; - - /// Set properties from another frame - void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); } - - protected: - /* POWER */ - bool get_power_() const { return this->pbuf_[11] & 0x01; } - void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); } -}; - -// Query state frame (read-only) -class QueryFrame : public midea_dongle::StaticFrame { - public: - QueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Power query state frame (read-only) -class PowerQueryFrame : public midea_dongle::StaticFrame { - public: - PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Command frame -class CommandFrame : public midea_dongle::StaticFrame { - public: - CommandFrame() : StaticFrame(FPSTR(this->INIT)) {} - void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py deleted file mode 100644 index daa8ea6657..0000000000 --- a/esphome/components/midea_dongle/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import uart -from esphome.const import CONF_ID - -DEPENDENCIES = ["wifi", "uart"] -CODEOWNERS = ["@dudanov"] - -midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle") -MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice) - -CONF_MIDEA_DONGLE_ID = "midea_dongle_id" -CONF_STRENGTH_ICON = "strength_icon" -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MideaDongle), - cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean, - } - ) - .extend(cv.COMPONENT_SCHEMA) - .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) - cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON])) diff --git a/esphome/components/midea_dongle/midea_dongle.cpp b/esphome/components/midea_dongle/midea_dongle.cpp deleted file mode 100644 index 7e3683a964..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "midea_dongle.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace midea_dongle { - -static const char *const TAG = "midea_dongle"; - -void MideaDongle::loop() { - while (this->available()) { - const uint8_t rx = this->read(); - if (this->idx_ <= OFFSET_LENGTH) { - if (this->idx_ == OFFSET_LENGTH) { - if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) { - this->reset_(); - continue; - } - this->cnt_ = rx; - } else if (rx != SYNC_BYTE) { - continue; - } - } - this->buf_[this->idx_++] = rx; - if (--this->cnt_) - continue; - this->reset_(); - const BaseFrame frame(this->buf_); - ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str()); - if (!frame.is_valid()) { - ESP_LOGW(TAG, "RX: frame check failed!"); - continue; - } - if (frame.get_type() == QUERY_NETWORK) { - this->notify_.set_type(QUERY_NETWORK); - this->need_notify_ = true; - continue; - } - if (this->appliance_ != nullptr) - this->appliance_->on_frame(frame); - } -} - -void MideaDongle::update() { - const bool is_conn = WiFi.isConnected(); - uint8_t wifi_strength = 0; - if (!this->rssi_timer_) { - if (is_conn) - wifi_strength = 4; - } else if (is_conn) { - if (--this->rssi_timer_) { - wifi_strength = this->notify_.get_signal_strength(); - } else { - this->rssi_timer_ = 60; - const int32_t dbm = WiFi.RSSI(); - if (dbm > -63) - wifi_strength = 4; - else if (dbm > -75) - wifi_strength = 3; - else if (dbm > -88) - wifi_strength = 2; - else if (dbm > -100) - wifi_strength = 1; - } - } else { - this->rssi_timer_ = 1; - } - if (this->notify_.is_connected() != is_conn) { - this->notify_.set_connected(is_conn); - this->need_notify_ = true; - } - if (this->notify_.get_signal_strength() != wifi_strength) { - this->notify_.set_signal_strength(wifi_strength); - this->need_notify_ = true; - } - if (!--this->notify_timer_) { - this->notify_.set_type(NETWORK_NOTIFY); - this->need_notify_ = true; - } - if (this->need_notify_) { - ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength); - this->need_notify_ = false; - this->notify_timer_ = 600; - this->notify_.finalize(); - this->write_frame(this->notify_); - return; - } - if (this->appliance_ != nullptr) - this->appliance_->on_update(); -} - -void MideaDongle::write_frame(const Frame &frame) { - this->write_array(frame.data(), frame.size()); - ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str()); -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_dongle.h b/esphome/components/midea_dongle/midea_dongle.h deleted file mode 100644 index a7dfb9cf25..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/wifi/wifi_component.h" -#include "esphome/components/uart/uart.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF }; -enum MideaMessageType : uint8_t { - DEVICE_CONTROL = 0x02, - DEVICE_QUERY = 0x03, - NETWORK_NOTIFY = 0x0D, - QUERY_NETWORK = 0x63, -}; - -struct MideaAppliance { - /// Calling on update event - virtual void on_update() = 0; - /// Calling on frame receive event - virtual void on_frame(const Frame &frame) = 0; -}; - -class MideaDongle : public PollingComponent, public uart::UARTDevice { - public: - MideaDongle() : PollingComponent(1000) {} - float get_setup_priority() const override { return setup_priority::LATE; } - void update() override; - void loop() override; - void set_appliance(MideaAppliance *app) { this->appliance_ = app; } - void use_strength_icon(bool state) { this->rssi_timer_ = state; } - void write_frame(const Frame &frame); - - protected: - MideaAppliance *appliance_{nullptr}; - NotifyFrame notify_; - unsigned notify_timer_{1}; - // Buffer - uint8_t buf_[36]; - // Index - uint8_t idx_{0}; - // Reverse receive counter - uint8_t cnt_{2}; - uint8_t rssi_timer_{0}; - bool need_notify_{false}; - - // Reset receiver state - void reset_() { - this->idx_ = 0; - this->cnt_ = 2; - } -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.cpp b/esphome/components/midea_dongle/midea_frame.cpp deleted file mode 100644 index acb3feee5f..0000000000 --- a/esphome/components/midea_dongle/midea_frame.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -const uint8_t BaseFrame::CRC_TABLE[] = { - 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21, - 0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, - 0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C, - 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66, - 0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, - 0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6, - 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED, - 0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, - 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1, - 0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, - 0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57, - 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B, - 0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, - 0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35}; - -const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); } - -void BaseFrame::finalize() { - this->update_crc_(); - this->update_cs_(); -} - -void BaseFrame::update_crc_() { - uint8_t crc = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - uint8_t len = this->length_() - OFFSET_BODY; - while (--len) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++)); - *ptr = crc; -} - -void BaseFrame::update_cs_() { - uint8_t cs = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - uint8_t len = this->length_(); - while (--len) - cs -= *ptr++; - *ptr = cs; -} - -bool BaseFrame::has_valid_crc_() const { - uint8_t crc = 0; - uint8_t len = this->length_() - OFFSET_BODY; - const uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - for (; len; ptr++, len--) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr)); - return !crc; -} - -bool BaseFrame::has_valid_cs_() const { - uint8_t cs = 0; - uint8_t len = this->length_(); - const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - for (; len; ptr++, len--) - cs -= *ptr; - return !cs; -} - -void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) { - uint8_t *dst = this->pbuf_ + idx; - if (state) - *dst |= mask; - else - *dst &= ~mask; -} - -static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); } - -String Frame::to_string() const { - String ret; - char buf[4]; - buf[2] = ' '; - buf[3] = '\0'; - ret.reserve(3 * 36); - const uint8_t *it = this->data(); - for (size_t i = 0; i < this->size(); i++, it++) { - buf[0] = u4hex(*it >> 4); - buf[1] = u4hex(*it & 15); - ret.concat(buf); - } - return ret; -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.h b/esphome/components/midea_dongle/midea_frame.h deleted file mode 100644 index ce89cc636e..0000000000 --- a/esphome/components/midea_dongle/midea_frame.h +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -#include "esphome/core/component.h" - -namespace esphome { -namespace midea_dongle { - -static const uint8_t OFFSET_START = 0; -static const uint8_t OFFSET_LENGTH = 1; -static const uint8_t OFFSET_APPTYPE = 2; -static const uint8_t OFFSET_BODY = 10; -static const uint8_t SYNC_BYTE = 0xAA; - -class Frame { - public: - Frame() = delete; - Frame(uint8_t *data) : pbuf_(data) {} - Frame(const Frame &frame) : pbuf_(frame.data()) {} - - // Frame buffer - uint8_t *data() const { return this->pbuf_; } - // Frame size - uint8_t size() const { return this->length_() + OFFSET_LENGTH; } - uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; } - - template typename std::enable_if::value, T>::type as() const { - return T(*this); - } - String to_string() const; - - protected: - uint8_t *pbuf_; - uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; } -}; - -class BaseFrame : public Frame { - public: - BaseFrame() = delete; - BaseFrame(uint8_t *data) : Frame(data) {} - BaseFrame(const Frame &frame) : Frame(frame) {} - - // Check for valid - bool is_valid() const; - // Prepare for sending to device - void finalize(); - uint8_t get_type() const { return this->pbuf_[9]; } - void set_type(uint8_t value) { this->pbuf_[9] = value; } - bool has_response_type(uint8_t type) const { return this->resp_type_() == type; } - bool has_type(uint8_t type) const { return this->get_type() == type; } - - protected: - static const uint8_t PROGMEM CRC_TABLE[256]; - void set_bytemask_(uint8_t idx, uint8_t mask, bool state); - uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; } - bool has_valid_crc_() const; - bool has_valid_cs_() const; - void update_crc_(); - void update_cs_(); -}; - -template class StaticFrame : public T { - public: - // Default constructor - StaticFrame() : T(this->buf_) {} - // Copy constructor - StaticFrame(const Frame &src) : T(this->buf_) { - if (src.length_() < sizeof(this->buf_)) { - memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH); - } - } - // Constructor for RAM data - StaticFrame(const uint8_t *src) : T(this->buf_) { - const uint8_t len = src[OFFSET_LENGTH]; - if (len < sizeof(this->buf_)) { - memcpy(this->buf_, src, len + OFFSET_LENGTH); - } - } - // Constructor for PROGMEM data - StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) { - const uint8_t *src = reinterpret_cast(pgm); - const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH); - if (len < sizeof(this->buf_)) { - memcpy_P(this->buf_, src, len + OFFSET_LENGTH); - } - } - - protected: - uint8_t buf_[buf_size]; -}; - -// Device network notification frame -class NotifyFrame : public midea_dongle::StaticFrame { - public: - NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {} - void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; } - uint8_t get_signal_strength() const { return this->pbuf_[12]; } - void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; } - bool is_connected() const { return !this->pbuf_[18]; } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index d0f07e42dc..43397770d1 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -33,7 +33,7 @@ void MitsubishiClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state[6] = MITSUBISHI_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[6] = MITSUBISHI_AUTO; break; case climate::CLIMATE_MODE_OFF: @@ -42,8 +42,8 @@ void MitsubishiClimate::transmit_state() { break; } - remote_state[7] = - (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN); + 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]); diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 6b454cbaf0..254322d097 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -2,7 +2,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.cpp_helpers import gpio_pin_expression from esphome.components import uart -from esphome.const import CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_ADDRESS +from esphome.const import ( + CONF_FLOW_CONTROL_PIN, + CONF_ID, + CONF_ADDRESS, +) from esphome import pins DEPENDENCIES = ["uart"] @@ -13,11 +17,16 @@ ModbusDevice = modbus_ns.class_("ModbusDevice") MULTI_CONF = True CONF_MODBUS_ID = "modbus_id" +CONF_SEND_WAIT_TIME = "send_wait_time" + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Modbus), cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_SEND_WAIT_TIME, default="250ms" + ): cv.positive_time_period_milliseconds, } ) .extend(cv.COMPONENT_SCHEMA) @@ -36,6 +45,9 @@ async def to_code(config): pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) cg.add(var.set_flow_control_pin(pin)) + if CONF_SEND_WAIT_TIME in config: + cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + def modbus_device_schema(default_address): schema = { diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 2d714e72a2..1f6d868baf 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1,5 +1,6 @@ #include "modbus.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus { @@ -13,10 +14,15 @@ 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; @@ -49,48 +55,66 @@ 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); // Byte 0: modbus address (match all) if (at == 0) return true; uint8_t address = raw[0]; - - // Byte 1: Function (msb indicates error) - if (at == 1) - return (byte & 0x80) != 0x80; - + uint8_t function_code = raw[1]; // Byte 2: Size (with modbus rtu function code 4/3) // See also https://en.wikipedia.org/wiki/Modbus if (at == 2) return true; uint8_t data_len = raw[2]; - // Byte 3..3+data_len-1: Data - if (at < 3 + data_len) + uint8_t data_offset = 3; + // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } + + // Error ( msb indicates error ) + // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc + if ((function_code & 0x80) == 0x80) { + data_offset = 2; + data_len = 1; + } + + // Byte data_offset..data_offset+data_len-1: Data + if (at < data_offset + data_len) return true; // Byte 3+data_len: CRC_LO (over all bytes) - if (at == 3 + data_len) + if (at == data_offset + data_len) return true; - // Byte 3+len+1: CRC_HI (over all bytes) - uint16_t computed_crc = crc16(raw, 3 + data_len); - uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8); + + // Byte data_offset+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); if (computed_crc != remote_crc) { ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); return false; } - std::vector data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + waiting_for_response = 0; + std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { - device->on_modbus_data(data); + // Is it an error response? + if ((function_code & 0x80) == 0x80) { + ESP_LOGW(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); + device->on_modbus_error(function_code & 0x7F, raw[2]); + } else { + device->on_modbus_data(data); + } found = true; } } if (!found) { - ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); } // return false to reset buffer @@ -100,31 +124,79 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:"); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); + ESP_LOGCONFIG(TAG, " Send Wait Time: %d ms", this->send_wait_time_); } float Modbus::get_setup_priority() const { // After UART bus return setup_priority::BUS - 1.0f; } -void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { - uint8_t frame[8]; - frame[0] = address; - frame[1] = function; - frame[2] = start_address >> 8; - frame[3] = start_address >> 0; - frame[4] = register_count >> 8; - frame[5] = register_count >> 0; - auto crc = crc16(frame, 6); - frame[6] = crc >> 0; - frame[7] = crc >> 8; + +void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, + uint8_t payload_len, const uint8_t *payload) { + static const size_t MAX_VALUES = 128; + + if (number_of_entities > MAX_VALUES) { + ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); + return; + } + + 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 (payload != nullptr) { + if (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 + } + for (int i = 0; i < payload_len; i++) { + data.push_back(payload[i]); + } + } + + auto crc = crc16(data.data(), data.size()); + data.push_back(crc >> 0); + data.push_back(crc >> 8); if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(true); - this->write_array(frame, 8); + this->write_array(data); this->flush(); if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); + waiting_for_response = address; + last_send_ = millis(); + ESP_LOGV(TAG, "Modbus write: %s", hexencode(data).c_str()); +} + +// Helper function for lambdas +// Send raw command. Except CRC everything must be contained in payload +void Modbus::send_raw(const std::vector &payload) { + if (payload.empty()) { + return; + } + + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(true); + + auto crc = crc16(payload.data(), payload.size()); + this->write_array(payload); + this->write_byte(crc & 0xFF); + this->write_byte((crc >> 8) & 0xFF); + this->flush(); + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(false); + waiting_for_response = payload[0]; + last_send_ = millis(); } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 876c46b688..400e29e08b 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -22,17 +22,21 @@ class Modbus : public uart::UARTDevice, public Component { float get_setup_priority() const override; - void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); - + 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_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; } protected: GPIOPin *flow_control_pin_{nullptr}; bool parse_modbus_byte_(uint8_t byte); - + uint16_t send_wait_time_{250}; std::vector rx_buffer_; uint32_t last_modbus_byte_{0}; + uint32_t last_send_{0}; std::vector devices_; }; @@ -43,10 +47,14 @@ class ModbusDevice { void set_parent(Modbus *parent) { parent_ = parent; } void set_address(uint8_t address) { address_ = address; } virtual void on_modbus_data(const std::vector &data) = 0; - - void send(uint8_t function, uint16_t start_address, uint16_t register_count) { - this->parent_->send(this->address_, function, start_address, register_count); + virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + 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); } + void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } + // If more than one device is connected block sending a new command before a response is received + bool waiting_for_response() { return parent_->waiting_for_response != 0; } protected: friend Modbus; diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py new file mode 100644 index 0000000000..7a69029dab --- /dev/null +++ b/esphome/components/modbus_controller/__init__.py @@ -0,0 +1,114 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import modbus +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.cpp_helpers import logging +from .const import ( + CONF_COMMAND_THROTTLE, +) + +CODEOWNERS = ["@martgras"] + +AUTO_LOAD = ["modbus"] + +MULTI_CONF = True + +# pylint: disable=invalid-name +modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") +ModbusController = modbus_controller_ns.class_( + "ModbusController", cg.PollingComponent, modbus.ModbusDevice +) + +SensorItem = modbus_controller_ns.struct("SensorItem") + +ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") +MODBUS_FUNCTION_CODE = { + "read_coils": ModbusFunctionCode.READ_COILS, + "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, + "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, + "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, + "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, + "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, + "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, + "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, +} + +ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") +ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") +MODBUS_REGISTER_TYPE = { + "coil": ModbusRegisterType.COIL, + "discrete_input": ModbusRegisterType.DISCRETE, + "holding": ModbusRegisterType.HOLDING, + "read": ModbusRegisterType.READ, +} + +SensorValueType_ns = modbus_controller_ns.namespace("SensorValueType") +SensorValueType = SensorValueType_ns.enum("SensorValueType") +SENSOR_VALUE_TYPE = { + "RAW": SensorValueType.RAW, + "U_WORD": SensorValueType.U_WORD, + "S_WORD": SensorValueType.S_WORD, + "U_DWORD": SensorValueType.U_DWORD, + "U_DWORD_R": SensorValueType.U_DWORD_R, + "S_DWORD": SensorValueType.S_DWORD, + "S_DWORD_R": SensorValueType.S_DWORD_R, + "U_QWORD": SensorValueType.U_QWORD, + "U_QWORDU_R": SensorValueType.U_QWORD_R, + "S_QWORD": SensorValueType.S_QWORD, + "U_QWORD_R": SensorValueType.S_QWORD_R, + "FP32": SensorValueType.FP32, + "FP32_R": SensorValueType.FP32_R, +} + + +MULTI_CONF = True + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ModbusController), + cv.Optional( + CONF_COMMAND_THROTTLE, default="0ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_COMMAND_THROTTLE]) + cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + await register_modbus_device(var, config) + + +async def register_modbus_device(var, config): + cg.add(var.set_address(config[CONF_ADDRESS])) + await cg.register_component(var, config) + return await modbus.register_modbus_device(var, config) + + +def function_code_to_register(function_code): + FUNCTION_CODE_TYPE_MAP = { + "read_coils": ModbusRegisterType.COIL, + "read_discrete_inputs": ModbusRegisterType.DISCRETE, + "read_holding_registers": ModbusRegisterType.HOLDING, + "read_input_registers": ModbusRegisterType.READ, + "write_single_coil": ModbusRegisterType.COIL, + "write_single_register": ModbusRegisterType.HOLDING, + "write_multiple_coils": ModbusRegisterType.COIL, + "write_multiple_registers": ModbusRegisterType.HOLDING, + } + return FUNCTION_CODE_TYPE_MAP[function_code] + + +def find_by_value(dict, find_value): + for (key, value) in MODBUS_REGISTER_TYPE.items(): + print(find_value, value) + if find_value == value: + return key + return "not found" diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py new file mode 100644 index 0000000000..d46ff71f2d --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -0,0 +1,81 @@ +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, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusBinarySensor = modbus_controller_ns.class_( + "ModbusBinarySensor", cg.Component, binary_sensor.BinarySensor, SensorItem +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinarySensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusBinarySensor.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_template(template_)) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp new file mode 100644 index 0000000000..81066b3f5c --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -0,0 +1,40 @@ +#include "modbus_binarysensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.binary_sensor"; + +void ModbusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Modbus Controller Binary Sensor", this); } + +void ModbusBinarySensor::parse_and_publish(const std::vector &data) { + bool value; + + switch (this->register_type) { + case ModbusRegisterType::DISCRETE_INPUT: + value = coil_from_vector(this->offset, data); + break; + case ModbusRegisterType::COIL: + // offset for coil is the actual number of the coil not the byte offset + value = coil_from_vector(this->offset, data); + break; + default: + value = get_data(data, this->offset) & this->bitmask; + break; + } + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } + } + this->publish_state(value); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h new file mode 100644 index 0000000000..c516d6b916 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem { + public: + ModbusBinarySensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + uint8_t skip_updates, bool force_new_range) + : Component(), binary_sensor::BinarySensor() { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + + if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) + this->register_count = offset + 1; + else + this->register_count = 1; + } + + void parse_and_publish(const std::vector &data) override; + void set_state(bool state) { this->state = state; } + + void dump_config() override; + + using transform_func_t = + optional(ModbusBinarySensor *, bool, const std::vector &)>>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + transform_func_t transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py new file mode 100644 index 0000000000..3cd114e673 --- /dev/null +++ b/esphome/components/modbus_controller/const.py @@ -0,0 +1,13 @@ +CONF_BITMASK = "bitmask" +CONF_BYTE_OFFSET = "byte_offset" +CONF_COMMAND_THROTTLE = "command_throttle" +CONF_FORCE_NEW_RANGE = "force_new_range" +CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" +CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" +CONF_RAW_ENCODE = "raw_encode" +CONF_REGISTER_COUNT = "register_count" +CONF_REGISTER_TYPE = "register_type" +CONF_RESPONSE_SIZE = "response_size" +CONF_SKIP_UPDATES = "skip_updates" +CONF_VALUE_TYPE = "value_type" +CONF_WRITE_LAMBDA = "write_lambda" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp new file mode 100644 index 0000000000..70b5bf8eae --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -0,0 +1,559 @@ +#include "modbus_controller.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller"; + +void ModbusController::setup() { + // Modbus::setup(); + this->create_register_ranges_(); +} + +/* + To work with the existing modbus class and avoid polling for responses a command queue is used. + send_next_command will submit the command at the top of the queue and set the corresponding callback + to handle the response from the device. + Once the response has been processed it is removed from the queue and the next command is sent +*/ +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(); + + 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(); + if (!command->on_data_func) { // No handler remove from queue directly after sending + command_queue_.pop_front(); + } + } + return (!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) { + // 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(); + } +} + +// Dispatch the response to the registered handler +void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { + ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address, + response->payload.size()); + response->on_data_func(response->register_type, response->register_address, response->payload); +} + +void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { + ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); + // Remove pending command waiting for a response + auto ¤t_command = this->command_queue_.front(); + if (current_command != nullptr) { + ESP_LOGE(TAG, + "Modbus error - last command: function code=0x%X register adddress = 0x%X " + "registers count=%d " + "payload size=%zu", + function_code, current_command->register_address, current_command->register_count, + current_command->payload.size()); + command_queue_.pop_front(); + } +} + +void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); + + auto vec_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { + return (r.start_address == start_address && r.register_type == register_type); + }); + + if (vec_it == register_ranges_.end()) { + ESP_LOGE(TAG, "Handle incoming data : No matching range for sensor found - start_address : 0x%X", start_address); + return; + } + auto map_it = sensormap_.find(vec_it->first_sensorkey); + if (map_it == sensormap_.end()) { + ESP_LOGE(TAG, "Handle incoming data : No sensor found in at start_address : 0x%X (0x%llX)", start_address, + vec_it->first_sensorkey); + return; + } + // loop through all sensors with the same start address + while (map_it != sensormap_.end() && map_it->second->start_address == start_address) { + if (map_it->second->register_type == register_type) { + map_it->second->parse_and_publish(data); + } + map_it++; + } +} + +void ModbusController::queue_command(const ModbusCommandItem &command) { + // check if this commmand is already qeued. + // not very effective but the queue is never really large + for (auto &item : command_queue_) { + if (item->register_address == command.register_address && item->register_count == command.register_count && + item->register_type == command.register_type) { + ESP_LOGW(TAG, "Duplicate modbus command found"); + // update the payload of the queued command + // replaces a previous command + item->payload = command.payload; + return; + } + } + command_queue_.push_back(make_unique(command)); +} + +void ModbusController::update_range_(RegisterRange &r) { + ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type, + r.skip_updates_counter); + if (r.skip_updates_counter == 0) { + ModbusCommandItem command_item = + ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count); + queue_command(command_item); + r.skip_updates_counter = r.skip_updates; // reset counter to config value + } else { + r.skip_updates_counter--; + } +} +// +// Queue the modbus requests to be send. +// 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()); + } else { + ESP_LOGV(TAG, "Updating modbus component"); + } + + for (auto &r : this->register_ranges_) { + ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address); + update_range_(r); + } +} + +// walk through the sensors and determine the registerranges to read +size_t ModbusController::create_register_ranges_() { + register_ranges_.clear(); + uint8_t n = 0; + if (sensormap_.empty()) { + return 0; + } + + auto ix = sensormap_.begin(); + auto prev = ix; + int total_register_count = 0; + uint16_t current_start_address = ix->second->start_address; + uint8_t buffer_offset = ix->second->offset; + uint8_t skip_updates = ix->second->skip_updates; + auto first_sensorkey = ix->second->getkey(); + total_register_count = 0; + while (ix != sensormap_.end()) { + ESP_LOGV(TAG, "Register: 0x%X %d %d 0x%llx (%d) buffer_offset = %d (0x%X) skip=%u", ix->second->start_address, + ix->second->register_count, ix->second->offset, ix->second->getkey(), total_register_count, buffer_offset, + buffer_offset, ix->second->skip_updates); + // if this is a sequential address based on number of registers and address of previous sensor + // convert to an offset to the previous sensor (address 0x101 becomes address 0x100 offset 2 bytes) + if (!ix->second->force_new_range && total_register_count >= 0 && + prev->second->register_type == ix->second->register_type && + prev->second->start_address + total_register_count == ix->second->start_address && + prev->second->start_address < ix->second->start_address) { + ix->second->start_address = prev->second->start_address; + ix->second->offset += prev->second->offset + prev->second->get_register_size(); + + // replace entry in sensormap_ + auto const value = ix->second; + sensormap_.erase(ix); + sensormap_.insert({value->getkey(), value}); + // move iterator back to new element + ix = sensormap_.find(value->getkey()); // next(prev, 1); + } + if (current_start_address != ix->second->start_address || + // ( prev->second->start_address + prev->second->offset != ix->second->start_address) || + ix->second->register_type != prev->second->register_type) { + // Difference doesn't match so we have a gap + if (n > 0) { + RegisterRange r; + r.start_address = current_start_address; + r.register_count = total_register_count; + if (prev->second->register_type == ModbusRegisterType::COIL || + prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { + r.register_count = prev->second->offset + 1; + } + r.register_type = prev->second->register_type; + r.first_sensorkey = first_sensorkey; + r.skip_updates = skip_updates; + r.skip_updates_counter = 0; + ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + } + skip_updates = ix->second->skip_updates; + current_start_address = ix->second->start_address; + first_sensorkey = ix->second->getkey(); + total_register_count = ix->second->register_count; + buffer_offset = ix->second->offset; + n = 1; + } else { + n++; + if (ix->second->offset != prev->second->offset || n == 1) { + total_register_count += ix->second->register_count; + buffer_offset += ix->second->get_register_size(); + } + // use the lowest non zero value for the whole range + // Because zero is the default value for skip_updates it is excluded from getting the min value. + if (ix->second->skip_updates != 0) { + if (skip_updates != 0) { + skip_updates = std::min(skip_updates, ix->second->skip_updates); + } else { + skip_updates = ix->second->skip_updates; + } + } + } + prev = ix++; + } + // Add the last range + if (n > 0) { + RegisterRange r; + r.start_address = current_start_address; + // r.register_count = prev->second->offset>>1 + prev->second->get_register_size(); + r.register_count = total_register_count; + if (prev->second->register_type == ModbusRegisterType::COIL || + prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { + r.register_count = prev->second->offset + 1; + } + r.register_type = prev->second->register_type; + r.first_sensorkey = first_sensorkey; + r.skip_updates = skip_updates; + r.skip_updates_counter = 0; + ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + } + return register_ranges_.size(); +} + +void ModbusController::dump_config() { + ESP_LOGCONFIG(TAG, "ModbusController:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGCONFIG(TAG, "sensormap"); + for (auto &it : sensormap_) { + ESP_LOGCONFIG("TAG", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), it.second->start_address, + it.second->register_count, it.second->get_register_size()); + } +#endif +} + +void ModbusController::loop() { + // Incoming data to process? + if (!incoming_queue_.empty()) { + auto &message = incoming_queue_.front(); + if (message != nullptr) + process_modbus_data_(message.get()); + incoming_queue_.pop(); + + } else { + // all messages processed send pending commmands + send_next_command_(); + } +} + +void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data(data, 0), get_data(data, 1)); +} + +void ModbusController::dump_sensormap_() { + ESP_LOGV("modbuscontroller.h", "sensormap"); + for (auto &it : sensormap_) { + ESP_LOGV("modbuscontroller.h", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), + it.second->start_address, it.second->register_count, it.second->get_register_size()); + } +} + +ModbusCommandItem ModbusCommandItem::create_read_command( + ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> + &&handler) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = register_type; + cmd.function_code = modbus_register_read_function(register_type); + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = std::move(handler); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbusdevice, + ModbusRegisterType register_type, uint16_t start_address, + uint16_t register_count) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = register_type; + cmd.function_code = modbus_register_read_function(register_type); + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_register_data(register_type, start_address, data); + }; + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusController *modbusdevice, + uint16_t start_address, uint16_t register_count, + const std::vector &values) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::HOLDING; + cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS; + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + for (auto v : values) { + cmd.payload.push_back((v / 256) & 0xFF); + cmd.payload.push_back(v & 0xFF); + } + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_single_coil(ModbusController *modbusdevice, uint16_t address, + bool value) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::COIL; + cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_COIL; + cmd.register_address = address; + cmd.register_count = 1; + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + cmd.payload.push_back(value ? 0xFF : 0); + cmd.payload.push_back(0); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, + const std::vector &values) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::COIL; + cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_COILS; + cmd.register_address = start_address; + cmd.register_count = values.size(); + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + + uint8_t bitmask = 0; + int bitcounter = 0; + for (auto coil : values) { + if (coil) { + bitmask |= (1 << bitcounter); + } + bitcounter++; + if (bitcounter % 8 == 0) { + cmd.payload.push_back(bitmask); + bitmask = 0; + } + } + // add remaining bits + if (bitcounter % 8) { + cmd.payload.push_back(bitmask); + } + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, + int16_t value) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::HOLDING; + cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_REGISTER; + cmd.register_address = start_address; + cmd.register_count = 1; // not used here anyways + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + cmd.payload.push_back((value / 256) & 0xFF); + cmd.payload.push_back((value % 256) & 0xFF); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.function_code = ModbusFunctionCode::CUSTOM; + if (handler == nullptr) { + cmd.on_data_func = [](ModbusRegisterType, uint16_t, const std::vector &data) { + ESP_LOGI(TAG, "Custom Command sent"); + }; + } else { + cmd.on_data_func = handler; + } + cmd.payload = values; + + return cmd; +} + +bool ModbusCommandItem::send() { + if (this->function_code != ModbusFunctionCode::CUSTOM) { + modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), + this->payload.empty() ? nullptr : &this->payload[0]); + } 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); + return true; +} + +std::vector float_to_payload(float value, SensorValueType value_type) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + std::vector data; + int32_t val; + + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + // cast truncates the float do some rounding here + data.push_back(lroundf(value) & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + val = lroundf(value); + data.push_back((val & 0xFFFF0000) >> 16); + data.push_back(val & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + val = lroundf(value); + data.push_back(val & 0xFFFF); + data.push_back((val & 0xFFFF0000) >> 16); + break; + case SensorValueType::FP32: + raw_to_float.float_value = value; + data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); + data.push_back(raw_to_float.raw & 0xFFFF); + break; + case SensorValueType::FP32_R: + raw_to_float.float_value = value; + data.push_back(raw_to_float.raw & 0xFFFF); + data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); + break; + default: + ESP_LOGE(TAG, "Invalid data type for modbus float to payload conversation"); + break; + } + return data; +} + +float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits + float result = NAN; + + switch (sensor_value_type) { + case SensorValueType::U_WORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; + result = static_cast(value); + break; + case SensorValueType::U_DWORD: + value = get_data(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + result = static_cast(value); + break; + case SensorValueType::U_DWORD_R: + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + result = static_cast(value); + break; + case SensorValueType::S_WORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), + bitmask); // default is 0xFFFF ; + result = static_cast(value); + break; + case SensorValueType::S_DWORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); + result = static_cast(value); + break; + case SensorValueType::S_DWORD_R: { + value = get_data(data, offset); + // Currently the high word is at the low position + // the sign bit is therefore at low before the switch + uint32_t sign_bit = (value & 0x8000) << 16; + value = mask_and_shift_by_rightbit( + static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); + result = static_cast(value); + } break; + case SensorValueType::U_QWORD: + // Ignore bitmask for U_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + + case SensorValueType::S_QWORD: + // Ignore bitmask for S_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + case SensorValueType::U_QWORD_R: + // Ignore bitmask for U_QWORD + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | + static_cast(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | + static_cast(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; + result = static_cast(value); + break; + + case SensorValueType::S_QWORD_R: + // Ignore bitmask for S_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + case SensorValueType::FP32: + raw_to_float.raw = get_data(data, offset); + ESP_LOGD(TAG, "FP32 = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); + result = raw_to_float.float_value; + break; + case SensorValueType::FP32_R: { + auto tmp = get_data(data, offset); + raw_to_float.raw = static_cast(tmp & 0xFFFF) << 16 | (tmp & 0xFFFF0000) >> 16; + ESP_LOGD(TAG, "FP32_R = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); + result = raw_to_float.float_value; + } break; + default: + break; + } + return result; +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h new file mode 100644 index 0000000000..4b5f4337db --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -0,0 +1,454 @@ +#pragma once + +#include "esphome/core/component.h" + +#include "esphome/core/automation.h" +#include "esphome/components/modbus/modbus.h" + +#include +#include +#include +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusController; + +enum class ModbusFunctionCode { + CUSTOM = 0x00, + READ_COILS = 0x01, + READ_DISCRETE_INPUTS = 0x02, + READ_HOLDING_REGISTERS = 0x03, + READ_INPUT_REGISTERS = 0x04, + WRITE_SINGLE_COIL = 0x05, + WRITE_SINGLE_REGISTER = 0x06, + READ_EXCEPTION_STATUS = 0x07, // not implemented + DIAGNOSTICS = 0x08, // not implemented + GET_COMM_EVENT_COUNTER = 0x0B, // not implemented + GET_COMM_EVENT_LOG = 0x0C, // not implemented + WRITE_MULTIPLE_COILS = 0x0F, + WRITE_MULTIPLE_REGISTERS = 0x10, + REPORT_SERVER_ID = 0x11, // not implemented + READ_FILE_RECORD = 0x14, // not implemented + WRITE_FILE_RECORD = 0x15, // not implemented + MASK_WRITE_REGISTER = 0x16, // not implemented + READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented + READ_FIFO_QUEUE = 0x18, // not implemented +}; + +enum class ModbusRegisterType : int { + CUSTOM = 0x0, + COIL = 0x01, + DISCRETE_INPUT = 0x02, + HOLDING = 0x03, + READ = 0x04, +}; + +enum class SensorValueType : uint8_t { + RAW = 0x00, // variable length + U_WORD = 0x1, // 1 Register unsigned + U_DWORD = 0x2, // 2 Registers unsigned + S_WORD = 0x3, // 1 Register signed + S_DWORD = 0x4, // 2 Registers signed + BIT = 0x5, + U_DWORD_R = 0x6, // 2 Registers unsigned + S_DWORD_R = 0x7, // 2 Registers unsigned + U_QWORD = 0x8, + S_QWORD = 0x9, + U_QWORD_R = 0xA, + S_QWORD_R = 0xB, + FP32 = 0xC, + FP32_R = 0xD +}; + +struct RegisterRange { + uint16_t start_address; + ModbusRegisterType register_type; + uint8_t register_count; + uint8_t skip_updates; // the config value + uint64_t first_sensorkey; + uint8_t skip_updates_counter; // the running value +} __attribute__((packed)); + +inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::READ_COILS; + break; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::READ_DISCRETE_INPUTS; + break; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_HOLDING_REGISTERS; + break; + case ModbusRegisterType::READ: + return ModbusFunctionCode::READ_INPUT_REGISTERS; + break; + default: + return ModbusFunctionCode::CUSTOM; + break; + } +} +inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::WRITE_SINGLE_COIL; + break; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::CUSTOM; + break; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; + break; + case ModbusRegisterType::READ: + return ModbusFunctionCode::CUSTOM; + break; + default: + return ModbusFunctionCode::CUSTOM; + break; + } +} + +/** All sensors are stored in a map + * to enable binary sensors for values encoded as bits in the same register the key of each sensor + * the key is a 64 bit integer that combines the register properties + * sensormap_ is sorted by this key. The key ensures the correct order when creating consequtive ranges + * Format: function_code (8 bit) | start address (16 bit)| offset (8bit)| bitmask (32 bit) + */ +inline uint64_t calc_key(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset = 0, + uint32_t bitmask = 0) { + return uint64_t((uint16_t(register_type) << 24) + (uint32_t(start_address) << 8) + (offset & 0xFF)) << 32 | bitmask; +} +inline uint16_t register_from_key(uint64_t key) { return (key >> 40) & 0xFFFF; } + +inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } + +/** Get a byte from a hex string + * hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 + * hex_byte_from_str("1122",0) returns 0x11 + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return byte value + */ +inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { + if (value.length() < pos * 2 + 1) + return 0; + return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); +} + +/** Get a word from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return word value + */ +inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { + return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); +} + +/** Get a dword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return dword value + */ +inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { + return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); +} + +/** Get a qword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return qword value + */ +inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { + return static_cast(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); +} + +// Extract data from modbus response buffer +/** Extract data from modbus response buffer + * @param T one of supported integer data types int_8,int_16,int_32,int_64 + * @param data modbus response buffer (uint8_t) + * @param buffer_offset offset in bytes. + * @return value of type T extracted from buffer + */ +template T get_data(const std::vector &data, size_t buffer_offset) { + if (sizeof(T) == sizeof(uint8_t)) { + return T(data[buffer_offset]); + } + if (sizeof(T) == sizeof(uint16_t)) { + return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); + } + + if (sizeof(T) == sizeof(uint32_t)) { + return get_data(data, buffer_offset) << 16 | get_data(data, (buffer_offset + 2)); + } + + if (sizeof(T) == sizeof(uint64_t)) { + return static_cast(get_data(data, buffer_offset)) << 32 | + (static_cast(get_data(data, buffer_offset + 4))); + } +} + +/** Extract coil data from modbus response buffer + * Responses for coil are packed into bytes . + * coil 3 is bit 3 of the first response byte + * coil 9 is bit 2 of the second response byte + * @param coil number of the cil + * @param data modbus response buffer (uint8_t) + * @return content of coil register + */ +inline bool coil_from_vector(int coil, const std::vector &data) { + auto data_byte = coil / 8; + return (data[data_byte] & (1 << (coil % 8))) > 0; +} + +/** Extract bits from value and shift right according to the bitmask + * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. + * the result is then shifted right by the postion if the first right set bit in the mask + * Usefull for modbus data where more than one value is packed in a 16 bit register + * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as + * D15 - D8 = hour, D7 - D0 = minute + * To get the hours use mask 0xFF00 and 0x00FF for the minute + * @param data an integral value between 16 aand 32 bits, + * @param bitmask the bitmask to apply + */ +template N mask_and_shift_by_rightbit(N data, uint32_t mask) { + auto result = (mask & data); + if (result == 0) { + return result; + } + for (int pos = 0; pos < sizeof(N) << 3; pos++) { + if ((mask & (1 << pos)) != 0) + return result >> pos; + } + return 0; +} + +/** convert float value to vector suitable for sending + * @param value float value to cconvert + * @param value_type defines if 16/32 or FP32 is used + * @return vector containing the modbus register words in correct order + */ +std::vector float_to_payload(float value, SensorValueType value_type); + +/** convert vector response payload to float + * @param value float value to cconvert + * @param sensor_value_type defines if 16/32/64 bits or FP32 is used + * @param offset offset to the data in data + * @param bitmask bitmask used for masking and shifting + * @return float version of the input + */ +float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); + +class ModbusController; + +struct SensorItem { + ModbusRegisterType register_type; + SensorValueType sensor_value_type; + uint16_t start_address; + uint32_t bitmask; + uint8_t offset; + uint8_t register_count; + uint8_t skip_updates; + bool force_new_range{false}; + + virtual void parse_and_publish(const std::vector &data) = 0; + + uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } + + size_t virtual get_register_size() const { + size_t size = 0; + switch (sensor_value_type) { + case SensorValueType::BIT: + size = 1; + break; + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + size = 2; + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32: + case SensorValueType::FP32_R: + size = 4; + break; + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + size = 8; + break; + case SensorValueType::RAW: + size = this->register_count * 2; + } + return size; + } +}; + +struct ModbusCommandItem { + static const size_t MAX_PAYLOAD_BYTES = 240; + ModbusController *modbusdevice; + uint16_t register_address; + uint16_t register_count; + ModbusFunctionCode function_code; + ModbusRegisterType register_type; + std::function &data)> + on_data_func; + std::vector payload = {}; + bool send(); + + /// factory methods + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param handler function called when the response is received + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_read_command( + ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> + &&handler); + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type, + uint16_t start_address, uint16_t register_count); + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param handler function called when the response is received + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address, + uint16_t register_count, const std::vector &values); + /** Create modbus write multiple registers command + * Function 16 (10hex) Write Multiple Registers + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param values uint16_t array to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, + int16_t value); + /** Create modbus write single registers command + * Function 05 (05hex) Write Single Coil + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param value uint16_t data to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value); + + /** Create modbus write multiple registers command + * Function 15 (0Fhex) Write Multiple Coils + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param value bool vector of values to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, + const std::vector &values); + /** Create custom modbus command + * @param modbusdevice pointer to the device to execute the command + * @param values byte vector of data to be sent to the device. The compplete payload must be provided with the + * exception of the crc codess + * @param handler function called when the response is received. Default is just logging a response + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler = nullptr); +}; + +/** Modbus controller class. + * Each instance handles the modbus commuinication for all sensors with the same modbus address + * + * all sensor items (sensors, switches, binarysensor ...) are parsed in modbus address ranges. + * when esphome calls ModbusController::Update the commands for each range are created and sent + * Responses for the commands are dispatched to the modbus sensor items. + */ + +class ModbusController : public PollingComponent, public modbus::ModbusDevice { + public: + ModbusController(uint16_t throttle = 0) : modbus::ModbusDevice(), command_throttle_(throttle){}; + void dump_config() override; + void loop() override; + void setup() override; + void update() override; + + /// queues a modbus command in the send queue + void queue_command(const ModbusCommandItem &command); + /// Registers a sensor with the controller. Called by esphomes code generator + void add_sensor_item(SensorItem *item) { sensormap_[item->getkey()] = item; } + /// called when a modbus response was prased 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; + /// 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); + /// called by esphome generated code to set the command_throttle period + void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; } + + protected: + /// parse sensormap_ and create range of sequential addresses + size_t create_register_ranges_(); + /// submit the read command for the address range to the send queue + void update_range_(RegisterRange &r); + /// parse incoming modbus data + 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_sensormap_(); + /// Collection of all sensors for this component + /// see calc_key how the key is contructed + std::map sensormap_; + /// Continous 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_; + /// when was the last send operation + uint32_t last_command_timestamp_; + /// min time in ms between sending modbus commands + uint16_t command_throttle_; +}; + +/** convert vector response payload to float + * @param value float value to cconvert + * @param item SensorItem object + * @return float version of the input + */ +inline float payload_to_float(const std::vector &data, const SensorItem &item) { + return payload_to_float(data, item.sensor_value_type, item.offset, item.bitmask); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py new file mode 100644 index 0000000000..c7919bb972 --- /dev/null +++ b/esphome/components/modbus_controller/number/__init__.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_LAMBDA, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MULTIPLY, + CONF_OFFSET, + CONF_STEP, +) + +from .. import ( + modbus_controller_ns, + ModbusController, + SENSOR_VALUE_TYPE, + SensorItem, +) + + +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusNumber = modbus_controller_ns.class_( + "ModbusNumber", cg.Component, number.Number, SensorItem +) + +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORDU_R": 4, + "S_QWORD": 4, + "U_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + if config[CONF_MIN_VALUE] < -16777215: + raise cv.Invalid("max_value must be greater than -16777215") + if config[CONF_MAX_VALUE] > 16777215: + raise cv.Invalid("max_value must not be greater than 16777215") + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusNumber), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.GenerateID(): cv.declare_id(ModbusNumber), + # 24 bits are the maximum value for fp32 before precison is lost + # 0x00FFFFFF = 16777215 + cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, + cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_, + cv.Optional(CONF_STEP, default=1): cv.positive_float, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + } + ).extend(cv.polling_component_schema("60s")), + validate_min_max, +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + value_type = config[CONF_VALUE_TYPE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_VALUE_TYPE], + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) + + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + + cg.add(var.set_parent(parent)) + cg.add(parent.add_sensor_item(var)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusNumber.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_template(template_)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusNumber.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp new file mode 100644 index 0000000000..95c6ac6f6a --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -0,0 +1,83 @@ +#include +#include "modbus_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus.number"; + +void ModbusNumber::parse_and_publish(const std::vector &data) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + float result = payload_to_float(data, *this); + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, result, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + result = val.value(); + } + } + ESP_LOGD(TAG, "Number new state : %.02f", result); + // this->sensor_->raw_state = result; + this->publish_state(result); +} + +void ModbusNumber::control(float value) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + std::vector data; + auto original_value = value; + // Is there are lambda configured? + if (this->write_transform_func_.has_value()) { + // data is passed by reference + // the lambda can fill the empty vector directly + // in that case the return value is ignored + auto val = (*this->write_transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } else { + value = multiply_by_ * value; + } + + // lambda didn't set payload + if (data.empty()) { + data = float_to_payload(value, this->sensor_value_type); + } + + ESP_LOGD(TAG, + "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", + this->get_name().c_str(), this->start_address, this->register_count, value, value); + + // Create and send the write command + auto write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + this->register_count, data); + + // publish new value + write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + // gets called when the write command is ack'd from the device + parent_->on_write_register_response(write_cmd.register_type, start_address, data); + this->publish_state(value); + }; + parent_->queue_command(write_cmd); +} +void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h new file mode 100644 index 0000000000..271bbfac50 --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +using value_to_data_t = std::function(float); + +class ModbusNumber : public number::Number, public Component, public SensorItem { + public: + ModbusNumber(uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, + uint8_t skip_updates, bool force_new_range) + : number::Number(), Component(), SensorItem() { + this->register_type = ModbusRegisterType::HOLDING; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->register_count = register_count; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + }; + + void dump_config() override; + void parse_and_publish(const std::vector &data) override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void set_update_interval(int) {} + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { multiply_by_ = factor; } + + using transform_func_t = std::function(ModbusNumber *, float, const std::vector &)>; + using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + + protected: + void control(float value) override; + optional transform_func_; + optional write_transform_func_; + ModbusController *parent_; + float multiply_by_{1.0}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py new file mode 100644 index 0000000000..9c41fc011c --- /dev/null +++ b/esphome/components/modbus_controller/output/__init__.py @@ -0,0 +1,74 @@ +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, + CONF_OFFSET, +) + +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, +) + +from ..const import ( + CONF_BYTE_OFFSET, + CONF_MODBUS_CONTROLLER_ID, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) +from ..sensor import SENSOR_VALUE_TYPE + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusOutput = modbus_controller_ns.class_( + "ModbusOutput", cg.Component, output.FloatOutput, SensorItem +) + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.GenerateID(): cv.declare_id(ModbusOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + } + ), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + var = cg.new_Pvariable( + config[CONF_ID], config[CONF_ADDRESS], byte_offset, config[CONF_VALUE_TYPE] + ) + await output.register_output(var, config) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(var.set_parent(parent)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusOutput.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp new file mode 100644 index 0000000000..f7d7c42342 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -0,0 +1,61 @@ +#include +#include "modbus_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.output"; + +void ModbusOutput::setup() {} + +/** Write a value to the device + * + */ +void ModbusOutput::write_state(float value) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + std::vector data; + auto original_value = value; + // Is there are lambda configured? + if (this->write_transform_func_.has_value()) { + // data is passed by reference + // the lambda can fill the empty vector directly + // in that case the return value is ignored + auto val = (*this->write_transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } else { + value = multiply_by_ * value; + } + // lambda didn't set payload + if (data.empty()) { + data = float_to_payload(value, this->sensor_value_type); + } + + ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", + this->start_address, this->register_count, value, original_value); + + // Create and send the write command + auto write_cmd = + ModbusCommandItem::create_write_multiple_command(parent_, this->start_address, this->register_count, data); + parent_->queue_command(write_cmd); +} + +void ModbusOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Float Output:"); + LOG_FLOAT_OUTPUT(this); + ESP_LOGCONFIG(TAG, "Modbus device start address=0x%X register count=%d value type=%hhu", this->start_address, + this->register_count, this->sensor_value_type); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h new file mode 100644 index 0000000000..053186a321 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +using value_to_data_t = std::function(float); + +class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { + public: + ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type) + : output::FloatOutput(), Component() { + this->register_type = ModbusRegisterType::HOLDING; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->skip_updates = 0; + this->start_address += offset; + this->offset = 0; + } + void setup() override; + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { multiply_by_ = factor; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + + protected: + void write_state(float value) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + float multiply_by_{1.0}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py new file mode 100644 index 0000000000..687f3d82fb --- /dev/null +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -0,0 +1,109 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusSensor = modbus_controller_ns.class_( + "ModbusSensor", cg.Component, sensor.Sensor, SensorItem +) + +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORDU_R": 4, + "S_QWORD": 4, + "U_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + + +CONFIG_SCHEMA = cv.All( + sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusSensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + value_type = config[CONF_VALUE_TYPE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_VALUE_TYPE], + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSensor.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_template(template_)) diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp new file mode 100644 index 0000000000..dbd0525347 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -0,0 +1,36 @@ + +#include "modbus_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.sensor"; + +void ModbusSensor::dump_config() { LOG_SENSOR(TAG, "Modbus Controller Sensor", this); } + +void ModbusSensor::parse_and_publish(const std::vector &data) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + float result = payload_to_float(data, *this); + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, result, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + result = val.value(); + } + } + ESP_LOGD(TAG, "Sensor new state: %.02f", result); + // this->sensor_->raw_state = result; + this->publish_state(result); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h new file mode 100644 index 0000000000..4f48c2a4dd --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusSensor : public Component, public sensor::Sensor, public SensorItem { + public: + ModbusSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) + : Component(), sensor::Sensor() { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->register_count = register_count; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + } + + void parse_and_publish(const std::vector &data) override; + void dump_config() override; + using transform_func_t = std::function(ModbusSensor *, float, const std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + optional transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py new file mode 100644 index 0000000000..e03b0d37be --- /dev/null +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -0,0 +1,81 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + MODBUS_REGISTER_TYPE, + SensorItem, + modbus_controller_ns, + ModbusController, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusSwitch = modbus_controller_ns.class_( + "ModbusSwitch", cg.Component, switch.Switch, SensorItem +) + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusSwitch), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + cg.add(var.set_parent(paren)) + if CONF_LAMBDA in config: + publish_template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSwitch.operator("ptr"), "item"), + (bool, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_template(publish_template_)) diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp new file mode 100644 index 0000000000..ce9557e6c4 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -0,0 +1,70 @@ + +#include "modbus_switch.h" +#include "esphome/core/log.h" +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.switch"; + +void ModbusSwitch::setup() { + // value isn't required + // without it we crash on save + this->get_initial_state(); +} +void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); } + +void ModbusSwitch::parse_and_publish(const std::vector &data) { + bool value = false; + switch (this->register_type) { + case ModbusRegisterType::DISCRETE_INPUT: + case ModbusRegisterType::COIL: + // offset for coil is the actual number of the coil not the byte offset + value = coil_from_vector(this->offset, data); + break; + default: + value = get_data(data, this->offset) & this->bitmask; + break; + } + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->publish_transform_func_) { + // the lambda can parse the response itself + auto val = (*this->publish_transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } + } + + ESP_LOGV(TAG, "Publish '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), + ONOFF(value), (int) this->register_type, this->start_address, this->offset); + this->publish_state(value); +} + +void ModbusSwitch::write_state(bool state) { + // This will be called every time the user requests a state change. + ModbusCommandItem cmd; + ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), + ONOFF(state), (int) this->register_type, this->start_address, this->offset); + switch (this->register_type) { + case ModbusRegisterType::COIL: + // offset for coil and discrete inputs is the coil/register number not bytes + cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); + break; + case ModbusRegisterType::DISCRETE_INPUT: + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, state); + break; + + default: + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, + state ? 0xFFFF & this->bitmask : 0); + break; + } + this->parent_->queue_command(cmd); + publish_state(state); +} +// ModbusSwitch end +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h new file mode 100644 index 0000000000..a38668fabb --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { + public: + ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + bool force_new_range) + : Component(), switch_::Switch() { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = 0; + this->register_count = 1; + if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) { + this->start_address += offset; + this->offset = 0; + } + this->force_new_range = force_new_range; + }; + void setup() override; + void write_state(bool state) override; + void dump_config() override; + void set_state(bool state) { this->state = state; } + void parse_and_publish(const std::vector &data) override; + void set_parent(ModbusController *parent) { this->parent_ = parent; } + + using transform_func_t = std::function(ModbusSwitch *, bool, const std::vector &)>; + void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } + + protected: + ModbusController *parent_; + optional publish_transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py new file mode 100644 index 0000000000..2c02c86795 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -0,0 +1,101 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_RESPONSE_SIZE, + CONF_SKIP_UPDATES, + CONF_RAW_ENCODE, + CONF_REGISTER_TYPE, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusTextSensor = modbus_controller_ns.class_( + "ModbusTextSensor", cg.Component, text_sensor.TextSensor, SensorItem +) + +RawEncoding_ns = modbus_controller_ns.namespace("RawEncoding") +RawEncoding = RawEncoding_ns.enum("RawEncoding") +RAW_ENCODING = { + "NONE": RawEncoding.NONE, + "HEXBYTES": RawEncoding.HEXBYTES, + "COMMA": RawEncoding.COMMA, +} + +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusTextSensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + 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_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + response_size = config[CONF_RESPONSE_SIZE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = response_size / 2 + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + reg_count, + config[CONF_RESPONSE_SIZE], + config[CONF_RAW_ENCODE], + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusTextSensor.operator("ptr"), "item"), + (cg.std_string.operator("const").operator("ref"), "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(cg.std_string), + ) + cg.add(var.set_template(template_)) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp new file mode 100644 index 0000000000..a06d44e90b --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -0,0 +1,56 @@ + +#include "modbus_textsensor.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.text_sensor"; + +void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); } + +void ModbusTextSensor::parse_and_publish(const std::vector &data) { + std::ostringstream output; + uint8_t max_items = this->response_bytes_; + char buffer[4]; + bool add_comma = false; + for (auto b : data) { + switch (this->encode_) { + case RawEncoding::HEXBYTES: + sprintf(buffer, "%02x", b); + output << buffer; + break; + case RawEncoding::COMMA: + sprintf(buffer, add_comma ? ",%d" : "%d", b); + output << buffer; + add_comma = true; + break; + // Anything else no encoding + case RawEncoding::NONE: + default: + output << (char) b; + break; + } + if (--max_items == 0) { + break; + } + } + + auto result = output.str(); + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, result, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + result = val.value(); + } + } + this->publish_state(result); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h new file mode 100644 index 0000000000..28d0f0b241 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2 }; + +class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem { + public: + ModbusTextSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, + uint16_t response_bytes, RawEncoding encode, uint8_t skip_updates, bool force_new_range) + : Component() { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->response_bytes_ = response_bytes; + this->register_count = register_count; + this->encode_ = encode; + this->skip_updates = skip_updates; + this->bitmask = 0xFFFFFFFF; + this->sensor_value_type = SensorValueType::RAW; + this->force_new_range = force_new_range; + } + size_t get_register_size() const override { + if (sensor_value_type == SensorValueType::RAW) { + return this->response_bytes_; + } else { + return SensorItem::get_register_size(); + } + } + + void dump_config() override; + + void parse_and_publish(const std::vector &data) override; + using transform_func_t = + std::function(ModbusTextSensor *, std::string, const std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + optional transform_func_{nullopt}; + + protected: + RawEncoding encode_; + uint16_t response_bytes_; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index c3a015ff3c..f1708ae70b 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -12,7 +12,7 @@ class MonochromaticLightOutput : public light::LightOutput { void set_output(output::FloatOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 274ed6dfec..7ba3da7b4d 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -1,5 +1,6 @@ #include "mpr121.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace mpr121 { diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index 05c26289b4..f9b61dcadc 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -4,10 +4,8 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_BRIEFCASE_DOWNLOAD, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER_PER_SECOND_SQUARED, ICON_SCREEN_ROTATION, @@ -30,21 +28,22 @@ MPU6050Component = mpu6050_ns.class_( ) accel_schema = sensor.sensor_schema( - UNIT_METER_PER_SECOND_SQUARED, - ICON_BRIEFCASE_DOWNLOAD, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED, + icon=ICON_BRIEFCASE_DOWNLOAD, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) gyro_schema = sensor.sensor_schema( - UNIT_DEGREE_PER_SECOND, - ICON_SCREEN_ROTATION, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREE_PER_SECOND, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) temperature_schema = sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 906c570b17..8f02f8d437 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -91,6 +91,8 @@ MQTTJSONLightComponent = mqtt_ns.class_("MQTTJSONLightComponent", MQTTComponent) MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) +MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) +MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) def validate_config(value): @@ -190,6 +192,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, + cv.only_with_arduino, ) @@ -212,7 +215,7 @@ async def to_code(config): await cg.register_component(var, config) # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library("AsyncMqttClient-esphome", "0.8.4") + cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.4") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) @@ -264,8 +267,7 @@ async def to_code(config): if CONF_SSL_FINGERPRINTS in config: for fingerprint in config[CONF_SSL_FINGERPRINTS]: arr = [ - cg.RawExpression("0x{}".format(fingerprint[i : i + 2])) - for i in range(0, 40, 2) + cg.RawExpression(f"0x{fingerprint[i:i + 2]}") for i in range(0, 40, 2) ] cg.add(var.add_ssl_fingerprint(arr)) cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") @@ -351,9 +353,7 @@ def get_default_topic_for(data, component_type, name, suffix): sanitized_name = "".join( x for x in name.lower().replace(" ", "_") if x in allowlist ) - return "{}/{}/{}/{}".format( - data.topic_prefix, component_type, sanitized_name, suffix - ) + return f"{data.topic_prefix}/{component_type}/{sanitized_name}/{suffix}" async def register_mqtt_component(var, config): diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp index 9dec9498ad..787cc1153f 100644 --- a/esphome/components/mqtt/custom_mqtt_device.cpp +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -1,4 +1,7 @@ #include "custom_mqtt_device.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" namespace esphome { @@ -28,3 +31,5 @@ bool CustomMQTTDevice::is_connected() { return global_mqtt_client != nullptr && } // namespace mqtt } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 1c8b2e916e..9795d69304 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_MQTT + #include "esphome/core/component.h" #include "mqtt_client.h" @@ -215,3 +218,5 @@ void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callba } // namespace mqtt } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 53a49c6844..188df0f7b9 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -1,6 +1,7 @@ #include "mqtt_binary_sensor.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_BINARY_SENSOR namespace esphome { @@ -9,6 +10,7 @@ namespace mqtt { static const char *const TAG = "mqtt.binary_sensor"; std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; } +const EntityBase *MQTTBinarySensorComponent::get_entity() const { return this->binary_sensor_; } void MQTTBinarySensorComponent::setup() { this->binary_sensor_->add_on_state_callback([this](bool state) { this->publish_state(state); }); @@ -24,7 +26,6 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic); } } -std::string MQTTBinarySensorComponent::friendly_name() const { return this->binary_sensor_->get_name(); } void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (!this->binary_sensor_->get_device_class().empty()) @@ -42,7 +43,6 @@ bool MQTTBinarySensorComponent::send_initial_state() { return true; } } -bool MQTTBinarySensorComponent::is_internal() { return this->binary_sensor_->is_internal(); } bool MQTTBinarySensorComponent::publish_state(bool state) { if (this->binary_sensor_->is_status_binary_sensor()) return true; @@ -55,3 +55,4 @@ bool MQTTBinarySensorComponent::publish_state(bool state) { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 1ca82a947e..0efb490367 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/defines.h" - +#ifdef USE_MQTT #ifdef USE_BINARY_SENSOR #include "mqtt_component.h" @@ -28,11 +28,10 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { bool send_initial_state() override; bool publish_state(bool state); - bool is_internal() override; protected: - std::string friendly_name() const override; std::string component_type() const override; + const EntityBase *get_entity() const override; binary_sensor::BinarySensor *binary_sensor_; }; @@ -41,3 +40,4 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index afb67d36ed..040b0001fe 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -1,9 +1,11 @@ #include "mqtt_client.h" +#ifdef USE_MQTT + #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/util.h" +#include "esphome/components/network/util.h" #include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -25,7 +27,7 @@ MQTTClientComponent::MQTTClientComponent() { // Connection void MQTTClientComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up MQTT..."); - this->mqtt_client_.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, + this->mqtt_client_.onMessage([this](char const *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { if (index == 0) this->payload_buffer_.reserve(total); @@ -60,7 +62,7 @@ void MQTTClientComponent::setup() { void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT:"); ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port, - this->ip_.toString().c_str()); + 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()); if (!this->discovery_info_.prefix.empty()) { @@ -87,11 +89,11 @@ void MQTTClientComponent::start_dnslookup_() { this->dns_resolve_error_ = false; this->dns_resolved_ = false; ip_addr_t addr; -#ifdef ARDUINO_ARCH_ESP32 - err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, this->dns_found_callback, this, - LWIP_DNS_ADDRTYPE_IPV4); +#ifdef USE_ESP32 + err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, + MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 err_t err = dns_gethostbyname(this->credentials_.address.c_str(), &addr, esphome::mqtt::MQTTClientComponent::dns_found_callback, this); #endif @@ -99,11 +101,11 @@ void MQTTClientComponent::start_dnslookup_() { case ERR_OK: { // Got IP immediately this->dns_resolved_ = true; -#ifdef ARDUINO_ARCH_ESP32 - this->ip_ = IPAddress(addr.u_addr.ip4.addr); +#ifdef USE_ESP32 + this->ip_ = addr.u_addr.ip4.addr; #endif -#ifdef ARDUINO_ARCH_ESP8266 - this->ip_ = IPAddress(addr.addr); +#ifdef USE_ESP8266 + this->ip_ = addr.addr; #endif this->start_connect_(); return; @@ -116,7 +118,7 @@ void MQTTClientComponent::start_dnslookup_() { default: case ERR_ARG: { // error -#if defined(ARDUINO_ARCH_ESP8266) +#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); @@ -143,10 +145,10 @@ void MQTTClientComponent::check_dnslookup_() { return; } - ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.toString().c_str()); + ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str().c_str()); this->start_connect_(); } -#if defined(ARDUINO_ARCH_ESP8266) && LWIP_VERSION_MAJOR == 1 +#if defined(USE_ESP8266) && LWIP_VERSION_MAJOR == 1 void MQTTClientComponent::dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg) { #else void MQTTClientComponent::dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) { @@ -155,18 +157,18 @@ void MQTTClientComponent::dns_found_callback(const char *name, const ip_addr_t * if (ipaddr == nullptr) { a_this->dns_resolve_error_ = true; } else { -#ifdef ARDUINO_ARCH_ESP32 - a_this->ip_ = IPAddress(ipaddr->u_addr.ip4.addr); +#ifdef USE_ESP32 + a_this->ip_ = ipaddr->u_addr.ip4.addr; #endif -#ifdef ARDUINO_ARCH_ESP8266 - a_this->ip_ = IPAddress(ipaddr->addr); +#ifdef USE_ESP8266 + a_this->ip_ = ipaddr->addr; #endif a_this->dns_resolved_ = true; } } void MQTTClientComponent::start_connect_() { - if (!network_is_connected()) + if (!network::is_connected()) return; ESP_LOGI(TAG, "Connecting to MQTT..."); @@ -183,7 +185,7 @@ void MQTTClientComponent::start_connect_() { this->mqtt_client_.setCredentials(username, password); - this->mqtt_client_.setServer(this->ip_, this->credentials_.port); + this->mqtt_client_.setServer((uint32_t) this->ip_, this->credentials_.port); if (!this->last_will_.topic.empty()) { this->mqtt_client_.setWill(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain, this->last_will_.payload.c_str(), this->last_will_.payload.length()); @@ -221,40 +223,40 @@ void MQTTClientComponent::check_connected() { void MQTTClientComponent::loop() { if (this->disconnect_reason_.has_value()) { - const char *reason_s = nullptr; + const LogString *reason_s; switch (*this->disconnect_reason_) { case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: - reason_s = "TCP disconnected"; + reason_s = LOG_STR("TCP disconnected"); break; case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - reason_s = "Unacceptable Protocol Version"; + reason_s = LOG_STR("Unacceptable Protocol Version"); break; case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: - reason_s = "Identifier Rejected"; + reason_s = LOG_STR("Identifier Rejected"); break; case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: - reason_s = "Server Unavailable"; + reason_s = LOG_STR("Server Unavailable"); break; case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: - reason_s = "Malformed Credentials"; + reason_s = LOG_STR("Malformed Credentials"); break; case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: - reason_s = "Not Authorized"; + reason_s = LOG_STR("Not Authorized"); break; case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: - reason_s = "Not Enough Space"; + reason_s = LOG_STR("Not Enough Space"); break; case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: - reason_s = "TLS Bad Fingerprint"; + reason_s = LOG_STR("TLS Bad Fingerprint"); break; default: - reason_s = "Unknown"; + reason_s = LOG_STR("Unknown"); break; } - if (!network_is_connected()) { - reason_s = "WiFi disconnected"; + if (!network::is_connected()) { + reason_s = LOG_STR("WiFi disconnected"); } - ESP_LOGW(TAG, "MQTT Disconnected: %s.", reason_s); + ESP_LOGW(TAG, "MQTT Disconnected: %s.", LOG_STR_ARG(reason_s)); this->disconnect_reason_.reset(); } @@ -477,7 +479,7 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 // on ESP8266, this is called in LWiP thread; some components do not like running // in an ISR. this->defer([this, topic, payload]() { @@ -485,7 +487,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string for (auto &subscription : this->subscriptions_) if (topic_match(topic.c_str(), subscription.topic.c_str())) subscription.callback(topic, payload); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 }); #endif } @@ -587,3 +589,5 @@ float MQTTMessageTrigger::get_setup_priority() const { return setup_priority::AF } // namespace mqtt } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 119e61db2b..fa689eaa04 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -1,10 +1,14 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/defines.h" + +#ifdef USE_MQTT + +#include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/core/log.h" #include "esphome/components/json/json_util.h" +#include "esphome/components/network/ip_address.h" #include #include "lwip/ip_addr.h" @@ -226,7 +230,7 @@ class MQTTClientComponent : public Component { void start_connect_(); void start_dnslookup_(); void check_dnslookup_(); -#if defined(ARDUINO_ARCH_ESP8266) && LWIP_VERSION_MAJOR == 1 +#if defined(USE_ESP8266) && LWIP_VERSION_MAJOR == 1 static void dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg); #else static void dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg); @@ -265,7 +269,7 @@ class MQTTClientComponent : public Component { std::vector subscriptions_; AsyncMqttClient mqtt_client_; MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; - IPAddress ip_; + network::IPAddress ip_; bool dns_resolved_{false}; bool dns_resolve_error_{false}; std::vector children_; @@ -352,3 +356,5 @@ template class MQTTConnectedCondition : public Condition } // namespace mqtt } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 164ba16faf..47b6684dec 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,6 +1,7 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_CLIMATE namespace esphome { @@ -60,8 +61,10 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC root["max_temp"] = traits.get_visual_max_temperature(); // temp_step root["temp_step"] = traits.get_visual_temperature_step(); + // temperature units are always coerced to Celsius internally + root["temp_unit"] = "C"; - if (traits.get_supports_away()) { + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { // away_mode_command_topic root["away_mode_cmd_t"] = this->get_away_command_topic(); // away_mode_state_topic @@ -72,7 +75,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC root["act_t"] = this->get_action_state_topic(); } - if (traits.get_supports_fan_modes()) { + if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { // fan_mode_command_topic root["fan_mode_cmd_t"] = this->get_fan_mode_command_topic(); // fan_mode_state_topic @@ -97,6 +100,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC fan_modes.add("focus"); if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) fan_modes.add("diffuse"); + for (const auto &fan_mode : traits.get_supported_custom_fan_modes()) + fan_modes.add(fan_mode); } if (traits.get_supports_swing_modes()) { @@ -164,19 +169,19 @@ void MQTTClimateComponent::setup() { }); } - if (traits.get_supports_away()) { + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { this->subscribe(this->get_away_command_topic(), [this](const std::string &topic, const std::string &payload) { auto onoff = parse_on_off(payload.c_str()); auto call = this->device_->make_call(); switch (onoff) { case PARSE_ON: - call.set_away(true); + call.set_preset(CLIMATE_PRESET_AWAY); break; case PARSE_OFF: - call.set_away(false); + call.set_preset(CLIMATE_PRESET_HOME); break; case PARSE_TOGGLE: - call.set_away(!this->device_->away); + call.set_preset(this->device_->preset == CLIMATE_PRESET_AWAY ? CLIMATE_PRESET_HOME : CLIMATE_PRESET_AWAY); break; case PARSE_NONE: default: @@ -207,9 +212,9 @@ void MQTTClimateComponent::setup() { } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); } -bool MQTTClimateComponent::is_internal() { return this->device_->is_internal(); } std::string MQTTClimateComponent::component_type() const { return "climate"; } -std::string MQTTClimateComponent::friendly_name() const { return this->device_->get_name(); } +const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; } + bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode @@ -241,7 +246,7 @@ bool MQTTClimateComponent::publish_state_() { if (!this->publish(this->get_mode_state_topic(), mode_s)) success = false; int8_t accuracy = traits.get_temperature_accuracy_decimals(); - if (traits.get_supports_current_temperature() && !isnan(this->device_->current_temperature)) { + if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) { std::string payload = value_accuracy_to_string(this->device_->current_temperature, accuracy); if (!this->publish(this->get_current_temperature_state_topic(), payload)) success = false; @@ -259,8 +264,8 @@ bool MQTTClimateComponent::publish_state_() { success = false; } - if (traits.get_supports_away()) { - std::string payload = ONOFF(this->device_->away); + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { + std::string payload = ONOFF(this->device_->preset == CLIMATE_PRESET_AWAY); if (!this->publish(this->get_away_state_topic(), payload)) success = false; } @@ -291,36 +296,39 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_fan_modes()) { - const char *payload = ""; - switch (this->device_->fan_mode.value()) { - case CLIMATE_FAN_ON: - payload = "on"; - break; - case CLIMATE_FAN_OFF: - payload = "off"; - break; - case CLIMATE_FAN_AUTO: - payload = "auto"; - break; - case CLIMATE_FAN_LOW: - payload = "low"; - break; - case CLIMATE_FAN_MEDIUM: - payload = "medium"; - break; - case CLIMATE_FAN_HIGH: - payload = "high"; - break; - case CLIMATE_FAN_MIDDLE: - payload = "middle"; - break; - case CLIMATE_FAN_FOCUS: - payload = "focus"; - break; - case CLIMATE_FAN_DIFFUSE: - payload = "diffuse"; - break; - } + std::string payload; + if (this->device_->fan_mode.has_value()) + switch (this->device_->fan_mode.value()) { + case CLIMATE_FAN_ON: + payload = "on"; + break; + case CLIMATE_FAN_OFF: + payload = "off"; + break; + case CLIMATE_FAN_AUTO: + payload = "auto"; + break; + case CLIMATE_FAN_LOW: + payload = "low"; + break; + case CLIMATE_FAN_MEDIUM: + payload = "medium"; + break; + case CLIMATE_FAN_HIGH: + payload = "high"; + break; + case CLIMATE_FAN_MIDDLE: + payload = "middle"; + break; + case CLIMATE_FAN_FOCUS: + payload = "focus"; + break; + case CLIMATE_FAN_DIFFUSE: + payload = "diffuse"; + break; + } + if (this->device_->custom_fan_mode.has_value()) + payload = this->device_->custom_fan_mode.value(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } @@ -352,3 +360,4 @@ bool MQTTClimateComponent::publish_state_() { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 8aea4feb26..40ac4c18c1 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" @@ -15,7 +16,6 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTTClimateComponent(climate::Climate *device); void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; - bool is_internal() override; std::string component_type() const override; void setup() override; @@ -37,7 +37,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, command) protected: - std::string friendly_name() const override; + const EntityBase *get_entity() const override; bool publish_state_(); @@ -48,3 +48,4 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 13acdcacd8..0ece4b3501 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -1,4 +1,7 @@ #include "mqtt_component.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -65,8 +68,13 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); - std::string name = this->friendly_name(); - root["name"] = name; + // Fields from EntityBase + root["name"] = this->friendly_name(); + if (this->is_disabled_by_default()) + root["enabled_by_default"] = false; + if (!this->get_icon().empty()) + root["icon"] = this->get_icon(); + if (config.state_topic) root["state_topic"] = this->get_state_topic_(); if (config.command_topic) @@ -102,9 +110,7 @@ bool MQTTComponent::send_discovery_() { device_info["identifiers"] = get_mac_address(); device_info["name"] = node_name; device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); -#ifdef ARDUINO_BOARD - device_info["model"] = ARDUINO_BOARD; -#endif + device_info["model"] = ESPHOME_BOARD; device_info["manufacturer"] = "espressif"; }, 0, discovery_info.retain); @@ -141,8 +147,7 @@ void MQTTComponent::set_custom_command_topic(const std::string &custom_command_t void MQTTComponent::set_availability(std::string topic, std::string payload_available, std::string payload_not_available) { - delete this->availability_; - this->availability_ = new Availability(); + this->availability_ = make_unique(); this->availability_->topic = std::move(topic); this->availability_->payload_available = std::move(payload_available); this->availability_->payload_not_available = std::move(payload_not_available); @@ -189,9 +194,23 @@ void MQTTComponent::call_loop() { this->schedule_resend_state(); } } +void MQTTComponent::call_dump_config() { + if (this->is_internal()) + return; + + this->dump_config(); +} void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } std::string MQTTComponent::unique_id() { return ""; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } +// Pull these properties from EntityBase if not overridden +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(); } + } // namespace mqtt } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 4d7a522d5f..657ab7b608 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -1,6 +1,13 @@ #pragma once +#include "esphome/core/defines.h" + +#ifdef USE_MQTT + +#include + #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "mqtt_client.h" namespace esphome { @@ -60,12 +67,14 @@ class MQTTComponent : public Component { void call_loop() override; + void call_dump_config() override; + /// Send discovery info the Home Assistant, override this. virtual void send_discovery(JsonObject &root, SendDiscoveryConfig &config) = 0; virtual bool send_initial_state() = 0; - virtual bool is_internal() = 0; + virtual bool is_internal(); /// Set whether state message should be retained. void set_retain(bool retain); @@ -140,8 +149,10 @@ class MQTTComponent : public Component { */ std::string get_default_topic_for_(const std::string &suffix) const; - /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const = 0; + /** + * Gets the Entity served by this MQTT component. + */ + virtual const EntityBase *get_entity() const = 0; /** A unique ID for this MQTT component, empty for no unique id. See unique ID requirements: * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements @@ -150,6 +161,15 @@ class MQTTComponent : public Component { */ virtual std::string unique_id(); + /// Get the friendly name of this MQTT component. + virtual std::string friendly_name() const; + + /// Get the icon field of this component + virtual std::string get_icon() const; + + /// Get whether the underlying Entity is disabled by default + virtual bool is_disabled_by_default() const; + /// Get the MQTT topic that new states will be shared to. const std::string get_state_topic_() const; @@ -171,9 +191,11 @@ class MQTTComponent : public Component { std::string custom_command_topic_{}; bool retain_{true}; bool discovery_enabled_{true}; - Availability *availability_{nullptr}; + std::unique_ptr availability_; bool resend_state_{false}; }; } // namespace mqtt } // namespace esphome + +#endif // USE_MQTt diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 25ac430abb..e8bc7f0e30 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,6 +1,7 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_COVER namespace esphome { @@ -61,11 +62,15 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->cover_->get_device_class().empty()) + root["device_class"] = this->cover_->get_device_class(); + auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root["optimistic"] = true; } if (traits.get_supports_position()) { + config.state_topic = false; root["position_topic"] = this->get_position_state_topic(); root["set_position_topic"] = this->get_position_command_topic(); } @@ -79,9 +84,9 @@ void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCon } std::string MQTTCoverComponent::component_type() const { return "cover"; } -std::string MQTTCoverComponent::friendly_name() const { return this->cover_->get_name(); } +const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; } + bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } -bool MQTTCoverComponent::is_internal() { return this->cover_->is_internal(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); bool success = true; @@ -112,3 +117,4 @@ bool MQTTCoverComponent::publish_state() { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 5c2ce93987..149d46ac85 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "mqtt_component.h" +#ifdef USE_MQTT #ifdef USE_COVER #include "esphome/components/cover/cover.h" @@ -23,7 +24,6 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(tilt, state) bool send_initial_state() override; - bool is_internal() override; bool publish_state(); @@ -31,7 +31,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { protected: std::string component_type() const override; - std::string friendly_name() const override; + const EntityBase *get_entity() const override; cover::Cover *cover_; }; @@ -40,3 +40,4 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 4171dae04c..898183cc58 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,6 +1,7 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_FAN #include "esphome/components/fan/fan_helpers.h" @@ -15,6 +16,8 @@ MQTTFanComponent::MQTTFanComponent(FanState *state) : MQTTComponent(), state_(st FanState *MQTTFanComponent::get_state() const { return this->state_; } std::string MQTTFanComponent::component_type() const { return "fan"; } +const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; } + void MQTTFanComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto val = parse_on_off(payload.c_str()); @@ -63,28 +66,67 @@ void MQTTFanComponent::setup() { }); } + if (this->state_->get_traits().supports_speed()) { + this->subscribe(this->get_speed_level_command_topic(), + [this](const std::string &topic, const std::string &payload) { + optional speed_level_opt = parse_int(payload); + if (speed_level_opt.has_value()) { + const int speed_level = speed_level_opt.value(); + if (speed_level >= 0 && speed_level <= this->state_->get_traits().supported_speed_count()) { + ESP_LOGD(TAG, "New speed level %d", speed_level); + this->state_->make_call().set_speed(speed_level).perform(); + } else { + ESP_LOGW(TAG, "Invalid speed level %d", speed_level); + this->status_momentary_warning("speed", 5000); + } + } else { + ESP_LOGW(TAG, "Invalid speed level %s (int expected)", payload.c_str()); + this->status_momentary_warning("speed", 5000); + } + }); + } + if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { - this->state_->make_call().set_speed(payload.c_str()).perform(); + this->state_->make_call() + .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) + .perform(); }); } auto f = std::bind(&MQTTFanComponent::publish_state, this); this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); } + +void MQTTFanComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Fan '%s': ", this->state_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); + if (this->state_->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation State Topic: '%s'", this->get_oscillation_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Oscillation Command Topic: '%s'", this->get_oscillation_command_topic().c_str()); + } + if (this->state_->get_traits().supports_speed()) { + ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str()); + } +} + bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } -std::string MQTTFanComponent::friendly_name() const { return this->state_->get_name(); } + void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (this->state_->get_traits().supports_oscillation()) { root["oscillation_command_topic"] = this->get_oscillation_command_topic(); root["oscillation_state_topic"] = this->get_oscillation_state_topic(); } if (this->state_->get_traits().supports_speed()) { + root["speed_level_command_topic"] = this->get_speed_level_command_topic(); + root["speed_level_state_topic"] = this->get_speed_level_state_topic(); root["speed_command_topic"] = this->get_speed_command_topic(); root["speed_state_topic"] = this->get_speed_state_topic(); } } -bool MQTTFanComponent::is_internal() { return this->state_->is_internal(); } bool MQTTFanComponent::publish_state() { const char *state_s = this->state_->state ? "ON" : "OFF"; ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s); @@ -96,19 +138,25 @@ bool MQTTFanComponent::publish_state() { failed = failed || !success; } auto traits = this->state_->get_traits(); + if (traits.supports_speed()) { + std::string payload = to_string(this->state_->speed); + bool success = this->publish(this->get_speed_level_state_topic(), payload); + failed = failed || !success; + } if (traits.supports_speed()) { const char *payload; + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { - case FAN_SPEED_LOW: { + case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "low"; break; } - case FAN_SPEED_MEDIUM: { + case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "medium"; break; } default: - case FAN_SPEED_HIGH: { + case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "high"; break; } @@ -124,3 +172,4 @@ bool MQTTFanComponent::publish_state() { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 5495780a27..a160d5366b 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_FAN #include "esphome/components/fan/fan_state.h" @@ -16,6 +17,8 @@ class MQTTFanComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, command) MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, state) + MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, command) + MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, state) MQTT_COMPONENT_CUSTOM_TOPIC(speed, command) MQTT_COMPONENT_CUSTOM_TOPIC(speed, state) @@ -25,6 +28,9 @@ class MQTTFanComponent : public mqtt::MQTTComponent { // (In most use cases you won't need these) /// Setup the fan subscriptions and discovery. void setup() override; + + void dump_config() override; + /// Send the full current state to MQTT. bool send_initial_state() override; bool publish_state(); @@ -33,10 +39,8 @@ class MQTTFanComponent : public mqtt::MQTTComponent { fan::FanState *get_state() const; - bool is_internal() override; - protected: - std::string friendly_name() const override; + const EntityBase *get_entity() const override; fan::FanState *state_; }; @@ -45,3 +49,4 @@ class MQTTFanComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 4bd0882b8c..a88358a6b2 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,8 +1,10 @@ #include "mqtt_light.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" namespace esphome { namespace mqtt { @@ -11,10 +13,13 @@ static const char *const TAG = "mqtt.light"; using namespace esphome::light; std::string MQTTJSONLightComponent::component_type() const { return "light"; } +const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; } void MQTTJSONLightComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) { - this->state_->make_call().parse_json(root).perform(); + LightCall call = this->state_->make_call(); + LightJSONSchema::parse_json(*this->state_, call, root); + call.perform(); }); auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); @@ -24,21 +29,39 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponent(), state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject &root) { this->state_->dump_json(root); }); + return this->publish_json(this->get_state_topic_(), + [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } -std::string MQTTJSONLightComponent::friendly_name() const { return this->state_->get_name(); } + void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); - if (traits.get_supports_brightness()) + + root["color_mode"] = true; + JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::WHITE)) + color_modes.add("white"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || + traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) + color_modes.add("color_temp"); + if (traits.supports_color_mode(ColorMode::RGB)) + color_modes.add("rgb"); + if (traits.supports_color_mode(ColorMode::RGB_WHITE) || + // HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now + traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE)) + color_modes.add("rgbw"); + if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) + color_modes.add("rgbww"); + + // legacy API + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; - if (traits.get_supports_rgb()) - root["rgb"] = true; - if (traits.get_supports_color_temperature()) - root["color_temp"] = true; - if (traits.get_supports_rgb_white_value()) - root["white_value"] = true; + if (this->state_->supports_effects()) { root["effect"] = true; JsonArray &effect_list = root.createNestedArray("effect_list"); @@ -48,7 +71,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover } } bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); } -bool MQTTJSONLightComponent::is_internal() { return this->state_->is_internal(); } void MQTTJSONLightComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str()); LOG_MQTT_COMPONENT(true, true) @@ -58,3 +80,4 @@ void MQTTJSONLightComponent::dump_config() { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 9417e71ddd..192cba39b6 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_LIGHT #include "mqtt_component.h" @@ -24,11 +25,9 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { bool send_initial_state() override; - bool is_internal() override; - protected: - std::string friendly_name() const override; std::string component_type() const override; + const EntityBase *get_entity() const override; bool publish_state_(); @@ -39,3 +38,4 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp new file mode 100644 index 0000000000..674fd77bdf --- /dev/null +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -0,0 +1,64 @@ +#include "mqtt_number.h" +#include "esphome/core/log.h" + +#ifdef USE_MQTT +#ifdef USE_NUMBER + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.number"; + +using namespace esphome::number; + +MQTTNumberComponent::MQTTNumberComponent(Number *number) : MQTTComponent(), number_(number) {} + +void MQTTNumberComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto val = parse_float(state); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); + return; + } + auto call = this->number_->make_call(); + call.set_value(*val); + call.perform(); + }); + this->number_->add_on_state_callback([this](float state) { this->publish_state(state); }); +} + +void MQTTNumberComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, false) +} + +std::string MQTTNumberComponent::component_type() const { return "number"; } +const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_; } + +void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + const auto &traits = number_->traits; + // https://www.home-assistant.io/integrations/number.mqtt/ + root["min"] = traits.get_min_value(); + root["max"] = traits.get_max_value(); + root["step"] = traits.get_step(); + + config.command_topic = true; +} +bool MQTTNumberComponent::send_initial_state() { + if (this->number_->has_state()) { + return this->publish_state(this->number_->state); + } else { + return true; + } +} +bool MQTTNumberComponent::publish_state(float value) { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + return this->publish(this->get_state_topic_(), buffer); +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h new file mode 100644 index 0000000000..66622d7c29 --- /dev/null +++ b/esphome/components/mqtt/mqtt_number.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_NUMBER + +#include "esphome/components/number/number.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTNumberComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTNumberComponent instance with the provided friendly_name and number + * + * @param number The number. + */ + explicit MQTTNumberComponent(number::Number *number); + + // ========== 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(float value); + + protected: + /// Override for MQTTComponent, returns "number". + std::string component_type() const override; + const EntityBase *get_entity() const override; + + number::Number *number_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp new file mode 100644 index 0000000000..b499636006 --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -0,0 +1,57 @@ +#include "mqtt_select.h" +#include "esphome/core/log.h" + +#ifdef USE_MQTT +#ifdef USE_SELECT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.select"; + +using namespace esphome::select; + +MQTTSelectComponent::MQTTSelectComponent(Select *select) : MQTTComponent(), select_(select) {} + +void MQTTSelectComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto call = this->select_->make_call(); + call.set_option(state); + call.perform(); + }); + this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); +} + +void MQTTSelectComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, false) +} + +std::string MQTTSelectComponent::component_type() const { return "select"; } +const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_; } + +void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + const auto &traits = select_->traits; + // https://www.home-assistant.io/integrations/select.mqtt/ + JsonArray &options = root.createNestedArray("options"); + for (const auto &option : traits.get_options()) + options.add(option); + + config.command_topic = true; +} +bool MQTTSelectComponent::send_initial_state() { + if (this->select_->has_state()) { + return this->publish_state(this->select_->state); + } else { + return true; + } +} +bool MQTTSelectComponent::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_select.h b/esphome/components/mqtt/mqtt_select.h new file mode 100644 index 0000000000..d77d0cf513 --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_SELECT + +#include "esphome/components/select/select.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTSelectComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTSelectComponent instance with the provided friendly_name and select + * + * @param select The select. + */ + explicit MQTTSelectComponent(select::Select *select); + + // ========== 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 "select". + std::string component_type() const override; + const EntityBase *get_entity() const override; + + select::Select *select_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 07c7fdc00d..78710ff403 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,6 +1,7 @@ #include "mqtt_sensor.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_SENSOR #ifdef USE_DEEP_SLEEP @@ -29,35 +30,32 @@ void MQTTSensorComponent::dump_config() { } std::string MQTTSensorComponent::component_type() const { return "sensor"; } +const EntityBase *MQTTSensorComponent::get_entity() const { return this->sensor_; } uint32_t MQTTSensorComponent::get_expire_after() const { - if (this->expire_after_.has_value()) { + if (this->expire_after_.has_value()) return *this->expire_after_; - } else { -#ifdef USE_DEEP_SLEEP - if (deep_sleep::global_has_deep_sleep) { - return 0; - } -#endif - return this->sensor_->calculate_expected_filter_update_interval() * 5; - } + return 0; } void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; } void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } -std::string MQTTSensorComponent::friendly_name() const { return this->sensor_->get_name(); } + void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->sensor_->get_device_class().empty()) + root["device_class"] = this->sensor_->get_device_class(); + if (!this->sensor_->get_unit_of_measurement().empty()) root["unit_of_measurement"] = this->sensor_->get_unit_of_measurement(); if (this->get_expire_after() > 0) root["expire_after"] = this->get_expire_after() / 1000; - if (!this->sensor_->get_icon().empty()) - root["icon"] = this->sensor_->get_icon(); - if (this->sensor_->get_force_update()) root["force_update"] = true; + if (this->sensor_->get_state_class() != STATE_CLASS_NONE) + root["state_class"] = state_class_to_string(this->sensor_->get_state_class()); + config.command_topic = false; } bool MQTTSensorComponent::send_initial_state() { @@ -67,7 +65,6 @@ bool MQTTSensorComponent::send_initial_state() { return true; } } -bool MQTTSensorComponent::is_internal() { return this->sensor_->is_internal(); } bool MQTTSensorComponent::publish_state(float value) { int8_t accuracy = this->sensor_->get_accuracy_decimals(); return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); @@ -78,3 +75,4 @@ std::string MQTTSensorComponent::unique_id() { return this->sensor_->unique_id() } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 8d8fa83531..22609fdfef 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -40,14 +41,11 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { bool publish_state(float value); bool send_initial_state() override; - bool is_internal() override; protected: /// Override for MQTTComponent, returns "sensor". std::string component_type() const override; - - std::string friendly_name() const override; - + const EntityBase *get_entity() const override; std::string unique_id() override; sensor::Sensor *sensor_; @@ -58,3 +56,4 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index b73e1ab8dc..16cf102f7e 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -1,6 +1,7 @@ #include "mqtt_switch.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_SWITCH namespace esphome { @@ -40,15 +41,13 @@ void MQTTSwitchComponent::dump_config() { } std::string MQTTSwitchComponent::component_type() const { return "switch"; } +const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { - if (!this->switch_->get_icon().empty()) - root["icon"] = this->switch_->get_icon(); if (this->switch_->assumed_state()) root["optimistic"] = true; } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } -bool MQTTSwitchComponent::is_internal() { return this->switch_->is_internal(); } -std::string MQTTSwitchComponent::friendly_name() const { return this->switch_->get_name(); } + bool MQTTSwitchComponent::publish_state(bool state) { const char *state_s = state ? "ON" : "OFF"; return this->publish(this->get_state_topic_(), state_s); @@ -58,3 +57,4 @@ bool MQTTSwitchComponent::publish_state(bool state) { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index 33b829c856..a0a7a23220 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_SWITCH #include "esphome/components/switch/switch.h" @@ -22,15 +23,13 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; - bool is_internal() override; bool publish_state(bool state); protected: - std::string friendly_name() const override; - /// "switch" component type. std::string component_type() const override; + const EntityBase *get_entity() const override; switch_::Switch *switch_; }; @@ -39,3 +38,4 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { } // 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 8bc11d954c..7b89915649 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -1,6 +1,7 @@ #include "mqtt_text_sensor.h" #include "esphome/core/log.h" +#ifdef USE_MQTT #ifdef USE_TEXT_SENSOR namespace esphome { @@ -12,9 +13,6 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : MQTTComponent(), sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { - if (!this->sensor_->get_icon().empty()) - root["icon"] = this->sensor_->get_icon(); - config.command_topic = false; } void MQTTTextSensor::setup() { @@ -34,12 +32,12 @@ bool MQTTTextSensor::send_initial_state() { return true; } } -bool MQTTTextSensor::is_internal() { return this->sensor_->is_internal(); } std::string MQTTTextSensor::component_type() const { return "sensor"; } -std::string MQTTTextSensor::friendly_name() const { return this->sensor_->get_name(); } +const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index a5ce0658c7..83743245cc 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" +#ifdef USE_MQTT #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" @@ -24,13 +25,9 @@ class MQTTTextSensor : public mqtt::MQTTComponent { bool send_initial_state() override; - bool is_internal() override; - protected: std::string component_type() const override; - - std::string friendly_name() const override; - + const EntityBase *get_entity() const override; std::string unique_id() override; text_sensor::TextSensor *sensor_; @@ -40,3 +37,4 @@ class MQTTTextSensor : public mqtt::MQTTComponent { } // namespace esphome #endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py index d640b254de..420d4f152c 100644 --- a/esphome/components/mqtt_subscribe/sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/sensor/__init__.py @@ -6,9 +6,6 @@ from esphome.const import ( CONF_QOS, CONF_TOPIC, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import mqtt_subscribe_ns @@ -21,7 +18,8 @@ MQTTSubscribeSensor = mqtt_subscribe_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index 06251bd7c8..e1accf3c70 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -1,4 +1,7 @@ #include "mqtt_subscribe_sensor.h" + +#ifdef USE_MQTT + #include "esphome/core/log.h" namespace esphome { @@ -31,3 +34,5 @@ void MQTTSubscribeSensor::dump_config() { } // namespace mqtt_subscribe } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h index a303ccad89..0619326ac9 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h @@ -1,5 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + +#ifdef USE_MQTT + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/mqtt/mqtt_client.h" @@ -25,3 +29,5 @@ class MQTTSubscribeSensor : public sensor::Sensor, public Component { } // namespace mqtt_subscribe } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp index 2b0908979c..8aa094a2d4 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp @@ -1,5 +1,7 @@ #include "mqtt_subscribe_text_sensor.h" +#ifdef USE_MQTT + #include "esphome/core/log.h" #include @@ -22,3 +24,5 @@ void MQTTSubscribeTextSensor::dump_config() { } // namespace mqtt_subscribe } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h index 69409f6348..9f8e5c63cc 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h @@ -1,5 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + +#ifdef USE_MQTT + #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/mqtt/mqtt_client.h" @@ -24,3 +28,5 @@ class MQTTSubscribeTextSensor : public text_sensor::TextSensor, public Component } // namespace mqtt_subscribe } // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 51dc569240..1d7516dbe8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -1,5 +1,6 @@ #include "ms5611.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ms5611 { diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index 34198e04eb..5decb13436 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ICON_GAUGE, @@ -26,18 +25,17 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MS5611Component), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index ee15f9743c..a777dcc960 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -1,8 +1,9 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" +#include namespace esphome { namespace my9231 { diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 59d784a614..0117f1b063 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -40,7 +40,7 @@ def validate_type(value): raise cv.Invalid("Must have B in type") rest = set(value) - set("RGBW") if rest: - raise cv.Invalid("Type has invalid color: {}".format(", ".join(rest))) + raise cv.Invalid(f"Type has invalid color: {', '.join(rest)}") if len(set(value)) != len(value): raise cv.Invalid("Type has duplicate color!") return value @@ -95,9 +95,7 @@ def validate_method_pin(value): for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): if opt in value and value[opt] not in pins_: raise cv.Invalid( - "Method {} only supports pin(s) {}".format( - method, ", ".join(f"GPIO{x}" for x in pins_) - ), + f"Method {method} only supports pin(s) {', '.join(f'GPIO{x}' for x in pins_)}", path=[CONF_METHOD], ) return value @@ -139,7 +137,7 @@ def format_method(config): if config[CONF_INVERT]: if method == "ESP8266_DMA": - variant = "Inverted" + variant + variant = f"Inverted{variant}" else: variant += "Inverted" @@ -170,14 +168,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VARIANT, default="800KBPS"): validate_variant, cv.Optional(CONF_METHOD, default=None): validate_method, cv.Optional(CONF_INVERT, default="no"): cv.boolean, - cv.Optional(CONF_PIN): pins.output_pin, - cv.Optional(CONF_CLOCK_PIN): pins.output_pin, - cv.Optional(CONF_DATA_PIN): pins.output_pin, + cv.Optional(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLOCK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_DATA_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, } ).extend(cv.COMPONENT_SCHEMA), _validate, validate_method_pin, + cv.only_with_arduino, ) @@ -205,4 +204,4 @@ async def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library("NeoPixelBus-esphome", "2.6.2") + cg.add_library("makuna/NeoPixelBus", "2.6.7") diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 2f279e1c9b..34e10f2cfe 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,13 +1,16 @@ #pragma once +#ifdef USE_ARDUINO + +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/color.h" #include "esphome/components/light/light_output.h" #include "esphome/components/light/addressable_light.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error The NeoPixelBus library requires at least arduino_core_version 2.4.x +#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error The NeoPixelBus library requires at least arduino_version 2.4.x #endif #include "NeoPixelBus.h" @@ -78,14 +81,11 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight { (*this)[i] = Color(0, 0, 0, 0); } - this->effect_data_ = new uint8_t[this->size()]; + this->effect_data_ = new uint8_t[this->size()]; // NOLINT this->controller_->Begin(); } - void loop() override { - if (!this->should_show_()) - return; - + void write_state(light::LightState *state) override { this->mark_shown_(); this->controller_->Dirty(); @@ -115,8 +115,7 @@ class NeoPixelRGBLightOutput : public NeoPixelBusLightOutputBase +#include +#include +#include + +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); + } + 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; + } + 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; + } + 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]; + } + uint8_t operator[](int index) const { return addr_[index]; } + uint8_t &operator[](int index) { return addr_[index]; } + + protected: + std::array addr_; +}; + +} // namespace network +} // namespace esphome diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp new file mode 100644 index 0000000000..f7ac6b543e --- /dev/null +++ b/esphome/components/network/util.cpp @@ -0,0 +1,54 @@ +#include "util.h" +#include "esphome/core/defines.h" + +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif + +namespace esphome { +namespace network { + +bool is_connected() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) + return true; +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_connected(); +#endif + + return false; +} + +network::IPAddress get_ip_address() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + return ethernet::global_eth_component->get_ip_address(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->get_ip_address(); +#endif + return {}; +} + +std::string get_use_address() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + return ethernet::global_eth_component->get_use_address(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->get_use_address(); +#endif + return ""; +} + +} // namespace network +} // namespace esphome diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h new file mode 100644 index 0000000000..f248d5cbf4 --- /dev/null +++ b/esphome/components/network/util.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "ip_address.h" + +namespace esphome { +namespace network { + +/// Return whether the node is connected to the network (through wifi, eth, ...) +bool is_connected(); +/// Get the active network hostname +std::string get_use_address(); +IPAddress get_ip_address(); + +} // namespace network +} // namespace esphome diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 67a49df9fa..924d58198d 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg +from esphome.components import uart nextion_ns = cg.esphome_ns.namespace("nextion") +Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) +nextion_ref = Nextion.operator("ref") + +CONF_NEXTION_ID = "nextion_id" diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h new file mode 100644 index 0000000000..5f4219acb1 --- /dev/null +++ b/esphome/components/nextion/automation.h @@ -0,0 +1,30 @@ +#pragma once +#include "esphome/core/automation.h" +#include "nextion.h" + +namespace esphome { +namespace nextion { + +class SetupTrigger : public Trigger<> { + public: + explicit SetupTrigger(Nextion *nextion) { + nextion->add_setup_state_callback([this]() { this->trigger(); }); + } +}; + +class SleepTrigger : public Trigger<> { + public: + explicit SleepTrigger(Nextion *nextion) { + nextion->add_sleep_state_callback([this]() { this->trigger(); }); + } +}; + +class WakeTrigger : public Trigger<> { + public: + explicit WakeTrigger(Nextion *nextion) { + nextion->add_wake_state_callback([this]() { this->trigger(); }); + } +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py new file mode 100644 index 0000000000..75694ee4b2 --- /dev/null +++ b/esphome/components/nextion/base_component.py @@ -0,0 +1,125 @@ +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 + +CONF_VARIABLE_NAME = "variable_name" +CONF_COMPONENT_NAME = "component_name" +CONF_WAVE_CHANNEL_ID = "wave_channel_id" +CONF_WAVE_MAX_VALUE = "wave_max_value" +CONF_PRECISION = "precision" +CONF_WAVEFORM_SEND_LAST_VALUE = "waveform_send_last_value" +CONF_TFT_URL = "tft_url" +CONF_ON_SLEEP = "on_sleep" +CONF_ON_WAKE = "on_wake" +CONF_ON_SETUP = "on_setup" +CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" +CONF_WAKE_UP_PAGE = "wake_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" + + +def NextionName(value): + 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." + ) + + return value + + +CONFIG_BASE_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_VISIBLE, default=True): cv.boolean, + } +) + + +CONFIG_TEXT_COMPONENT_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + +CONFIG_BINARY_SENSOR_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_VARIABLE_NAME): NextionName, + } + ) +) + +CONFIG_SENSOR_COMPONENT_SCHEMA = CONFIG_BINARY_SENSOR_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + + +CONFIG_SWITCH_COMPONENT_SCHEMA = CONFIG_SENSOR_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FOREGROUND_PRESSED_COLOR): cv.use_id(color), + cv.Optional(CONF_BACKGROUND_PRESSED_COLOR): cv.use_id(color), + } + ) +) + + +async def setup_component_core_(var, config, arg): + + if CONF_VARIABLE_NAME in config: + cg.add(var.set_variable_name(config[CONF_VARIABLE_NAME])) + elif CONF_COMPONENT_NAME in config: + cg.add( + var.set_variable_name( + config[CONF_COMPONENT_NAME], + config[CONF_COMPONENT_NAME] + arg, + ) + ) + + if CONF_BACKGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_COLOR]) + cg.add(var.set_background_color(color_component)) + + if CONF_BACKGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_PRESSED_COLOR]) + cg.add(var.set_background_pressed_color(color_component)) + + if CONF_FOREGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_COLOR]) + cg.add(var.set_foreground_color(color_component)) + + if CONF_FOREGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_PRESSED_COLOR]) + cg.add(var.set_foreground_pressed_color(color_component)) + + if CONF_FONT_ID in config: + cg.add(var.set_font_id(config[CONF_FONT_ID])) + + if CONF_VISIBLE in config: + cg.add(var.set_visible(config[CONF_VISIBLE])) diff --git a/esphome/components/nextion/binary_sensor.py b/esphome/components/nextion/binary_sensor.py deleted file mode 100644 index ed4e8d832a..0000000000 --- a/esphome/components/nextion/binary_sensor.py +++ /dev/null @@ -1,34 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import binary_sensor -from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID -from . import nextion_ns -from .display import Nextion - -DEPENDENCIES = ["display"] - -CONF_NEXTION_ID = "nextion_id" - -NextionTouchComponent = nextion_ns.class_( - "NextionTouchComponent", binary_sensor.BinarySensor -) - -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NextionTouchComponent), - cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), - cv.Required(CONF_PAGE_ID): cv.uint8_t, - cv.Required(CONF_COMPONENT_ID): cv.uint8_t, - } -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) - - hub = await cg.get_variable(config[CONF_NEXTION_ID]) - cg.add(hub.register_touch_component(var)) - - cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) - cg.add(var.set_page_id(config[CONF_PAGE_ID])) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py new file mode 100644 index 0000000000..090fae3429 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + + +from ..base_component import ( + setup_component_core_, + CONFIG_BINARY_SENSOR_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionBinarySensor = nextion_ns.class_( + "NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionBinarySensor), + cv.Optional(CONF_PAGE_ID): cv.uint8_t, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + } + ) + .extend(CONFIG_BINARY_SENSOR_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_at_least_one_key( + CONF_PAGE_ID, + CONF_COMPONENT_ID, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + ), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + + if config.keys() >= {CONF_PAGE_ID, CONF_COMPONENT_ID}: + cg.add(hub.register_touch_component(var)) + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + cg.add(var.set_page_id(config[CONF_PAGE_ID])) + + if CONF_COMPONENT_NAME in config or CONF_VARIABLE_NAME in config: + await setup_component_core_(var, config, ".val") + cg.add(hub.register_binarysensor_component(var)) diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp new file mode 100644 index 0000000000..c5bfa78efe --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -0,0 +1,69 @@ +#include "nextion_binarysensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_binarysensor"; + +void NextionBinarySensor::process_bool(const std::string &variable_name, bool state) { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + if (this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed binarysensor \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionBinarySensor::process_touch(uint8_t page_id, uint8_t component_id, bool state) { + if (this->page_id_ == page_id && this->component_id_ == component_id) { + this->publish_state(state); + } +} + +void NextionBinarySensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + this->nextion_->add_to_get_queue(shared_from_this()); +} + +void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (this->component_id_ == 0) // This is a legacy touch component + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %s", this->variable_name_.c_str(), + ONOFF(this->variable_name_.c_str())); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h new file mode 100644 index 0000000000..b86ee74013 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -0,0 +1,43 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionBinarySensor; + +class NextionBinarySensor : public NextionComponent, + public binary_sensor::BinarySensorInitiallyOff, + public PollingComponent, + public std::enable_shared_from_this { + public: + NextionBinarySensor(NextionBase *nextion) { this->nextion_ = nextion; } + + void update_component() override { this->update(); } + void update() override; + void send_state_to_nextion() override { this->set_state(this->state, false); }; + void process_bool(const std::string &variable_name, bool state) override; + void process_touch(uint8_t page_id, uint8_t component_id, bool state) override; + + // Set the components page id for Nextion Touch Component + void set_page_id(uint8_t page_id) { page_id_ = page_id; } + // Set the components component id for Nextion Touch Component + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + NextionQueueType get_queue_type() override { return NextionQueueType::BINARY_SENSOR; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + uint8_t page_id_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 7d7018a4c4..f4b35fd56f 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,20 +1,58 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS -from . import nextion_ns +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_BRIGHTNESS, + CONF_TRIGGER_ID, +) + +from . import Nextion, nextion_ns, nextion_ref +from .base_component import ( + CONF_ON_SLEEP, + CONF_ON_WAKE, + CONF_ON_SETUP, + CONF_TFT_URL, + CONF_TOUCH_SLEEP_TIMEOUT, + CONF_WAKE_UP_PAGE, + CONF_AUTO_WAKE_ON_TOUCH, +) + +CODEOWNERS = ["@senexcrenshaw"] DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor"] +AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] -Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) -NextionRef = Nextion.operator("ref") +SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) +SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) +WakeTrigger = nextion_ns.class_("WakeTrigger", 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_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_ON_SETUP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), + } + ), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), + } + ), + cv.Optional(CONF_ON_WAKE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), + } + ), + 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_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -31,8 +69,33 @@ async def to_code(config): cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(NextionRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + if CONF_TFT_URL in config: + cg.add_define("USE_NEXTION_TFT_UPLOAD") + cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + if CONF_TOUCH_SLEEP_TIMEOUT in config: + cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) + + 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])) + await display.register_display(var, config) + + for conf in config.get(CONF_ON_SETUP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_SLEEP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_WAKE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fe0767342b..d56c370412 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,5 +1,7 @@ #include "nextion.h" +#include "esphome/core/util.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace nextion { @@ -7,69 +9,171 @@ namespace nextion { static const char *const TAG = "nextion"; void Nextion::setup() { - this->send_command_no_ack(""); - this->send_command_printf("bkcmd=3"); - this->set_backlight_brightness(static_cast(brightness_ * 100)); - this->goto_page("0"); + this->is_setup_ = false; + this->ignore_is_setup_ = true; + + // Wake up the nextion + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + // Reboot it + this->send_command_("rest"); + + this->ignore_is_setup_ = false; } -float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } + +bool Nextion::send_command_(const std::string &command) { + if (!this->ignore_is_setup_ && !this->is_setup()) { + return false; + } + + ESP_LOGN(TAG, "send_command %s", command.c_str()); + + this->write_str(command.c_str()); + const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; + this->write_array(to_send, sizeof(to_send)); + return true; +} + +bool Nextion::check_connect_() { + if (this->get_is_connected_()) + 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 + this->send_command_("connect"); + + this->comok_sent_ = millis(); + this->ignore_is_setup_ = false; + + return false; + } + + if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + return false; + + std::string response; + + this->recv_ret_string_(response, 0, false); + if (response.empty() || response.find("comok") == std::string::npos) { +#ifdef NEXTION_PROTOCOL_LOG + ESP_LOGN(TAG, "Bad connect request %s", response.c_str()); + for (int i = 0; i < response.length(); i++) { + ESP_LOGN(TAG, "response %s %d %d %c", response.c_str(), i, response[i], response[i]); + } +#endif + + ESP_LOGW(TAG, "Nextion is not connected! "); + comok_sent_ = 0; + return false; + } + + this->ignore_is_setup_ = true; + ESP_LOGI(TAG, "Nextion is connected"); + this->is_connected_ = true; + + ESP_LOGN(TAG, "connect request %s", response.c_str()); + + size_t start; + size_t end = 0; + std::vector connect_info; + while ((start = response.find_first_not_of(',', end)) != std::string::npos) { + end = response.find(',', start); + connect_info.push_back(response.substr(start, end - start)); + } + + if (connect_info.size() == 7) { + ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); + + this->device_model_ = connect_info[2]; + this->firmware_version_ = connect_info[3]; + this->serial_number_ = connect_info[5]; + this->flash_size_ = connect_info[6]; + } else { + ESP_LOGE(TAG, "Nextion returned bad connect value \"%s\"", response.c_str()); + } + + this->ignore_is_setup_ = false; + this->dump_config(); + return true; +} + +void Nextion::reset_(bool reset_nextion) { + uint8_t d; + + while (this->available()) { // Clear receive buffer + this->read_byte(&d); + }; + this->nextion_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->touch_sleep_timeout_ != 0) { + ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + } +} + +float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { + if (!this->is_setup()) { + return; + } if (this->writer_.has_value()) { (*this->writer_)(*this); } } -void Nextion::send_command_no_ack(const char *command) { - // Flush RX... - this->loop(); - this->write_str(command); - const uint8_t data[3] = {0xFF, 0xFF, 0xFF}; - this->write_array(data, sizeof(data)); +void Nextion::add_sleep_state_callback(std::function &&callback) { + this->sleep_callback_.add(std::move(callback)); } -bool Nextion::ack_() { - if (!this->wait_for_ack_) - return true; +void Nextion::add_wake_state_callback(std::function &&callback) { + this->wake_callback_.add(std::move(callback)); +} - uint32_t start = millis(); - while (!this->read_until_ack_()) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Waiting for ACK timed out!"); - return false; - } +void Nextion::add_setup_state_callback(std::function &&callback) { + this->setup_callback_.add(std::move(callback)); +} + +void Nextion::update_all_components() { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->update_component(); + } + for (auto *sensortype : this->sensortype_) { + sensortype->update_component(); + } + for (auto *switchtype : this->switchtype_) { + switchtype->update_component(); + } + for (auto *textsensortype : this->textsensortype_) { + textsensortype->update_component(); } - return true; } -void Nextion::set_component_text(const char *component, const char *text) { - this->send_command_printf("%s.txt=\"%s\"", component, text); -} -void Nextion::set_component_value(const char *component, int value) { - this->send_command_printf("%s.val=%d", component, value); -} -void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id); -} -void Nextion::set_component_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco2=\"%s\"", component, color); -} -void Nextion::set_component_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco2=\"%s\"", component, color); -} -void Nextion::set_component_coordinates(const char *component, int x, int y) { - this->send_command_printf("%s.xcen=%d", component, x); - this->send_command_printf("%s.ycen=%d", component, y); -} -void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->send_command_printf("%s.font=%d", component, font_id); -} -void Nextion::goto_page(const char *page) { this->send_command_printf("page %s", page); } + bool Nextion::send_command_printf(const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + char buffer[256]; va_list arg; va_start(arg, format); @@ -79,208 +183,901 @@ bool Nextion::send_command_printf(const char *format, ...) { ESP_LOGW(TAG, "Building command for format '%s' failed!", format); return false; } - this->send_command_no_ack(buffer); - if (!this->ack_()) { - ESP_LOGW(TAG, "Sending command '%s' failed because no ACK was received", buffer); + + if (this->send_command_(buffer)) { + this->add_no_result_to_queue_("send_command_printf"); + return true; + } + return false; +} + +#ifdef NEXTION_PROTOCOL_LOG +void Nextion::print_queue_members_() { + ESP_LOGN(TAG, "print_queue_members_ (top 10) size %zu", this->nextion_queue_.size()); + ESP_LOGN(TAG, "*******************************************"); + int count = 0; + for (auto &i : this->nextion_queue_) { + if (count++ == 10) + break; + + if (i == nullptr) { + ESP_LOGN(TAG, "Nextion queue is null"); + } else { + ESP_LOGN(TAG, "Nextion queue type: %d:%s , name: %s", i->component->get_queue_type(), + i->component->get_queue_type_string().c_str(), i->component->get_variable_name().c_str()); + } + } + ESP_LOGN(TAG, "*******************************************"); +} +#endif + +void Nextion::loop() { + if (!this->check_connect_() || this->is_updating_) + return; + + if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) { + this->ignore_is_setup_ = true; + this->sent_setup_commands_ = true; + this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. + + this->set_backlight_brightness(this->brightness_); + this->goto_page("0"); + + this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + + if (this->touch_sleep_timeout_ != 0) { + this->set_touch_sleep_timeout(this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + this->set_wake_up_page(this->wake_up_page_); + } + + this->ignore_is_setup_ = false; + } + + this->process_serial_(); // Receive serial data + this->process_nextion_commands_(); // Process nextion return commands + + if (!this->nextion_reports_is_setup_) { + if (this->started_ms_ == 0) + this->started_ms_ = millis(); + + if (this->started_ms_ + this->startup_override_ms_ < millis()) { + ESP_LOGD(TAG, "Manually set nextion report ready"); + this->nextion_reports_is_setup_ = true; + } + } +} + +bool Nextion::remove_from_q_(bool report_empty) { + if (this->nextion_queue_.empty()) { + if (report_empty) + ESP_LOGE(TAG, "Nextion queue is empty!"); return false; } + auto nb = std::move(this->nextion_queue_.front()); + this->nextion_queue_.pop_front(); + auto &component = nb->component; + + ESP_LOGN(TAG, "Removing %s from the queue", component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + } + return true; } -void Nextion::hide_component(const char *component) { this->send_command_printf("vis %s,0", component); } -void Nextion::show_component(const char *component) { this->send_command_printf("vis %s,1", component); } -void Nextion::enable_component_touch(const char *component) { this->send_command_printf("tsw %s,1", component); } -void Nextion::disable_component_touch(const char *component) { this->send_command_printf("tsw %s,0", component); } -void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { - this->send_command_printf("add %d,%u,%u", component_id, channel_number, value); -} -void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("fill %d,%d,%d,%d,%s", x1, y1, width, height, color); -} -void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { - this->send_command_printf("line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); -} -void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); -} -void Nextion::circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cir %d,%d,%d,%s", center_x, center_y, radius, color); -} -void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cirs %d,%d,%d,%s", center_x, center_y, radius, color); -} -bool Nextion::read_until_ack_() { - while (this->available() >= 4) { - // flush preceding filler bytes - uint8_t temp; - while (this->available() && this->peek_byte(&temp) && temp == 0xFF) - this->read_byte(&temp); - if (!this->available()) - break; +void Nextion::process_serial_() { + uint8_t d; - uint8_t event; - // event type - this->read_byte(&event); + while (this->available()) { + read_byte(&d); + this->command_data_ += d; + } +} +// nextion.tech/instruction-set/ +void Nextion::process_nextion_commands_() { + if (this->command_data_.length() == 0) { + return; + } - uint8_t data[255]; - // total length of data (including end bytes) - uint8_t data_length = 0; - // message is terminated by three consecutive 0xFF - // this variable keeps track of ohow many of those have - // been received - uint8_t end_length = 0; - while (this->available() && end_length < 3 && data_length < sizeof(data)) { - uint8_t byte; - this->read_byte(&byte); - if (byte == 0xFF) { - end_length++; - } else { - end_length = 0; - } - data[data_length++] = byte; + size_t to_process_length = 0; + std::string to_process; + + ESP_LOGN(TAG, "this->command_data_ %s length %d", this->command_data_.c_str(), this->command_data_.length()); +#ifdef NEXTION_PROTOCOL_LOG + this->print_queue_members_(); +#endif + while ((to_process_length = this->command_data_.find(COMMAND_DELIMITER)) != std::string::npos) { + ESP_LOGN(TAG, "print_queue_members_ size %zu", this->nextion_queue_.size()); + while (to_process_length + COMMAND_DELIMITER.length() < this->command_data_.length() && + static_cast(this->command_data_[to_process_length + COMMAND_DELIMITER.length()]) == 0xFF) { + ++to_process_length; + ESP_LOGN(TAG, "Add extra 0xFF to process"); } - if (end_length != 3) { - ESP_LOGW(TAG, "Received unknown filler end bytes from Nextion!"); - continue; - } + this->nextion_event_ = this->command_data_[0]; - data_length -= 3; // remove filler bytes + to_process_length -= 1; + to_process = this->command_data_.substr(1, to_process_length); - bool invalid_data_length = false; - switch (event) { - case 0x01: // successful execution of instruction (ACK) - return true; - case 0x00: // invalid instruction + switch (this->nextion_event_) { + case 0x00: // instruction sent by user has failed ESP_LOGW(TAG, "Nextion reported invalid instruction!"); + this->remove_from_q_(); + break; - case 0x02: // component ID invalid - ESP_LOGW(TAG, "Nextion reported component ID invalid!"); + case 0x01: // instruction sent by user was successful + + ESP_LOGVV(TAG, "instruction sent by user was successful"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + + this->remove_from_q_(); + if (!this->is_setup_) { + if (this->nextion_queue_.empty()) { + ESP_LOGD(TAG, "Nextion is setup"); + this->is_setup_ = true; + this->setup_callback_.call(); + } + } + break; - case 0x03: // page ID invalid + case 0x02: // invalid Component ID or name was used + this->remove_from_q_(); + break; + case 0x03: // invalid Page ID or name was used ESP_LOGW(TAG, "Nextion reported page ID invalid!"); + this->remove_from_q_(); break; - case 0x04: // picture ID invalid + case 0x04: // invalid Picture ID was used ESP_LOGW(TAG, "Nextion reported picture ID invalid!"); + this->remove_from_q_(); break; - case 0x05: // font ID invalid + case 0x05: // invalid Font ID was used ESP_LOGW(TAG, "Nextion reported font ID invalid!"); + this->remove_from_q_(); break; - case 0x11: // baud rate setting invalid + case 0x06: // File operation fails + ESP_LOGW(TAG, "Nextion File operation fail!"); + break; + case 0x09: // Instructions with CRC validation fails their CRC check + ESP_LOGW(TAG, "Nextion Instructions with CRC validation fails their CRC check!"); + break; + case 0x11: // invalid Baud rate was used ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); break; - case 0x12: // curve control ID number or channel number is invalid - ESP_LOGW(TAG, "Nextion reported control/channel ID invalid!"); + case 0x12: // invalid Waveform ID or Channel # was used + + if (!this->nextion_queue_.empty()) { + int index = 0; + int found = -1; + for (auto &nb : this->nextion_queue_) { + auto &component = nb->component; + + 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()); + + found = index; + + 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!"); + } + } break; case 0x1A: // variable name invalid - ESP_LOGW(TAG, "Nextion reported variable name invalid!"); + this->remove_from_q_(); + break; case 0x1B: // variable operation invalid ESP_LOGW(TAG, "Nextion reported variable operation invalid!"); + this->remove_from_q_(); break; case 0x1C: // failed to assign ESP_LOGW(TAG, "Nextion reported failed to assign variable!"); + this->remove_from_q_(); break; case 0x1D: // operate EEPROM failed ESP_LOGW(TAG, "Nextion reported operating EEPROM failed!"); break; case 0x1E: // parameter quantity invalid ESP_LOGW(TAG, "Nextion reported parameter quantity invalid!"); + this->remove_from_q_(); break; case 0x1F: // IO operation failed ESP_LOGW(TAG, "Nextion reported component I/O operation invalid!"); break; case 0x20: // undefined escape characters ESP_LOGW(TAG, "Nextion reported undefined escape characters!"); + this->remove_from_q_(); break; case 0x23: // too long variable name ESP_LOGW(TAG, "Nextion reported too long variable name!"); + this->remove_from_q_(); + + break; + case 0x24: // Serial Buffer overflow occurs + ESP_LOGW(TAG, "Nextion reported Serial Buffer overflow!"); break; case 0x65: { // touch event return data - if (data_length != 3) { - invalid_data_length = true; + if (to_process_length != 3) { + ESP_LOGW(TAG, "Touch event data is expecting 3, received %zu", to_process_length); + break; } - uint8_t page_id = data[0]; - uint8_t component_id = data[1]; - uint8_t touch_event = data[2]; // 0 -> release, 1 -> press + 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"); for (auto *touch : this->touch_) { - touch->process(page_id, component_id, touch_event); + touch->process_touch(page_id, component_id, touch_event != 0); } break; } - case 0x67: - case 0x68: { // touch coordinate data - if (data_length != 5) { - invalid_data_length = true; + case 0x67: { // Touch Coordinate (awake) + break; + } + case 0x68: { // touch coordinate data (sleep) + + if (to_process_length != 5) { + ESP_LOGW(TAG, "Touch coordinate data is expecting 5, received %zu", to_process_length); + ESP_LOGW(TAG, "%s", to_process.c_str()); break; } - uint16_t x = (uint16_t(data[0]) << 8) | data[1]; - uint16_t y = (uint16_t(data[2]) << 8) | data[3]; - uint8_t touch_event = data[4]; // 0 -> release, 1 -> press + + 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"); break; } - case 0x66: // sendme page id + case 0x66: { + break; + } // sendme page id + + // 0x70 0x61 0x62 0x31 0x32 0x33 0xFF 0xFF 0xFF + // Returned when using get command for a string. + // Each byte is converted to char. + // data: ab123 case 0x70: // string variable data return + { + if (this->nextion_queue_.empty()) { + ESP_LOGW(TAG, "ERROR: Received string return but the queue is empty"); + break; + } + + auto nb = std::move(this->nextion_queue_.front()); + this->nextion_queue_.pop_front(); + auto &component = nb->component; + + if (component->get_queue_type() != NextionQueueType::TEXT_SENSOR) { + ESP_LOGE(TAG, "ERROR: Received string return but next in queue \"%s\" is not a text sensor", + component->get_variable_name().c_str()); + } else { + ESP_LOGN(TAG, "Received get_string response: \"%s\" for component id: %s, type: %s", to_process.c_str(), + component->get_variable_name().c_str(), component->get_queue_type_string().c_str()); + component->set_state_from_string(to_process, true, false); + } + + break; + } + // 0x71 0x01 0x02 0x03 0x04 0xFF 0xFF 0xFF + // Returned when get command to return a number + // 4 byte 32-bit value in little endian order. + // (0x01+0x02*256+0x03*65536+0x04*16777216) + // data: 67305985 case 0x71: // numeric variable data return - case 0x86: // device automatically enters into sleep mode + { + if (this->nextion_queue_.empty()) { + ESP_LOGE(TAG, "ERROR: Received numeric return but the queue is empty"); + break; + } + + if (to_process_length == 0) { + ESP_LOGE(TAG, "ERROR: Received numeric return but no data!"); + break; + } + + int dataindex = 0; + + int value = 0; + + for (int i = 0; i < 4; ++i) { + value += to_process[i] << (8 * i); + ++dataindex; + } + + auto nb = std::move(this->nextion_queue_.front()); + this->nextion_queue_.pop_front(); + auto &component = nb->component; + + if (component->get_queue_type() != NextionQueueType::SENSOR && + component->get_queue_type() != NextionQueueType::BINARY_SENSOR && + component->get_queue_type() != NextionQueueType::SWITCH) { + ESP_LOGE(TAG, "ERROR: Received numeric return but next in queue \"%s\" is not a valid sensor type %d", + component->get_variable_name().c_str(), component->get_queue_type()); + } else { + ESP_LOGN(TAG, "Received numeric return for variable %s, queue type %d:%s, value %d", + component->get_variable_name().c_str(), component->get_queue_type(), + component->get_queue_type_string().c_str(), value); + component->set_state_from_int(value, true, false); + } + + break; + } + + case 0x86: { // device automatically enters into sleep mode + ESP_LOGVV(TAG, "Received Nextion entering sleep automatically"); + this->is_sleeping_ = true; + this->sleep_callback_.call(); + break; + } case 0x87: // device automatically wakes up + { + ESP_LOGVV(TAG, "Received Nextion leaves sleep automatically"); + this->is_sleeping_ = false; + this->wake_callback_.call(); + this->all_components_send_state_(false); + break; + } case 0x88: // system successful start up - case 0x89: // start SD card upgrade - case 0xFD: // data transparent transmit finished - case 0xFE: // data transparent transmit ready + { + ESP_LOGD(TAG, "system successful start up %zu", to_process_length); + this->nextion_reports_is_setup_ = true; break; + } + case 0x89: { // start SD card upgrade + break; + } + // Data from nextion is + // 0x90 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x90: { // Switched component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad switch component data received for 0x90 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + 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); + + for (auto *switchtype : this->switchtype_) { + switchtype->process_bool(variable_name, to_process[index] != 0); + } + break; + } + // Data from nextion is + // 0x91 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x71 return data: prints temp1.val,0 + // FF FF FF - End + case 0x91: { // Sensor component + std::string variable_name; + uint8_t index = 0; + + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) != 4) { + ESP_LOGE(TAG, "Bad sensor component data received for 0x91 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + index = to_process.find('\0'); + variable_name = to_process.substr(0, index); + // // Get variable name + int value = 0; + for (int i = 0; i < 4; ++i) { + value += to_process[i + index + 1] << (8 * i); + } + + ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + + for (auto *sensor : this->sensortype_) { + sensor->process_sensor(variable_name, value); + } + break; + } + + // Data from nextion is + // 0x92 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x70 return formatted data (bytes) that contain the text prints temp1.txt,0 + // 00 - NULL + // FF FF FF - End + case 0x92: { // Text Sensor Component + std::string variable_name; + std::string text_value; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad text sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + 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()); + + // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; + // nq->variable_name = variable_name; + // nq->state = text_value; + // this->textsensorq_.push_back(nq); + for (auto *textsensortype : this->textsensortype_) { + textsensortype->process_text(variable_name, text_value); + } + break; + } + // Data from nextion is + // 0x93 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x93: { // Binary Sensor component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad binary sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + 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); + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); + } + break; + } + case 0xFD: { // data transparent transmit finished + ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); + 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; + break; + } + ++index; + } + + if (found == -1) { + ESP_LOGE(TAG, "No waveforms in queue to send data!"); + break; + } else { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } + break; + } default: - ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", event); + ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", this->nextion_event_); break; } - if (invalid_data_length) { - ESP_LOGW(TAG, "Invalid data length from nextion!"); + + // ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1); + this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); + } + + uint32_t ms = millis(); + + if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + for (int i = 0; i < this->nextion_queue_.size(); i++) { + auto &component = this->nextion_queue_[i]->component; + if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { + if (this->nextion_queue_[i]->queue_time == 0) + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\" queue_time 0", + component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); + + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\"", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + } + + this->nextion_queue_.erase(this->nextion_queue_.begin() + i); + i--; + + } else { + break; + } + } + } + ESP_LOGN(TAG, "Loop End"); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); +} // namespace nextion + +void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, float state) { + this->set_nextion_sensor_state(static_cast(queue_type), name, state); +} + +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); + + switch (queue_type) { + case NextionQueueType::SENSOR: { + for (auto *sensor : this->sensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } + break; + } + case NextionQueueType::BINARY_SENSOR: { + for (auto *sensor : this->binarysensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + case NextionQueueType::SWITCH: { + for (auto *sensor : this->switchtype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + default: { + ESP_LOGW(TAG, "set_nextion_sensor_state does not support a queue type %d", queue_type); + } + } +} + +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()); + + for (auto *sensor : this->textsensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } +} + +void Nextion::all_components_send_state_(bool force_update) { + ESP_LOGD(TAG, "all_components_send_state_ "); + for (auto *binarysensortype : this->binarysensortype_) { + if (force_update || binarysensortype->get_needs_to_send_update()) + binarysensortype->send_state_to_nextion(); + } + for (auto *sensortype : this->sensortype_) { + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_chan_id() == 0) + sensortype->send_state_to_nextion(); + } + for (auto *switchtype : this->switchtype_) { + if (force_update || switchtype->get_needs_to_send_update()) + switchtype->send_state_to_nextion(); + } + for (auto *textsensortype : this->textsensortype_) { + if (force_update || textsensortype->get_needs_to_send_update()) + textsensortype->send_state_to_nextion(); + } +} + +void Nextion::update_components_by_prefix(const std::string &prefix) { + for (auto *binarysensortype : this->binarysensortype_) { + if (binarysensortype->get_variable_name().find(prefix, 0) != std::string::npos) + binarysensortype->update_component_settings(true); + } + for (auto *sensortype : this->sensortype_) { + if (sensortype->get_variable_name().find(prefix, 0) != std::string::npos) + sensortype->update_component_settings(true); + } + for (auto *switchtype : this->switchtype_) { + if (switchtype->get_variable_name().find(prefix, 0) != std::string::npos) + switchtype->update_component_settings(true); + } + for (auto *textsensortype : this->textsensortype_) { + if (textsensortype->get_variable_name().find(prefix, 0) != std::string::npos) + textsensortype->update_component_settings(true); + } +} + +uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) { + uint16_t ret = 0; + uint8_t c = 0; + uint8_t nr_of_ff_bytes = 0; + uint64_t start; + bool exit_flag = false; + bool ff_flag = false; + + start = millis(); + + while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + this->read_byte(&c); + if (c == 0xFF) + nr_of_ff_bytes++; + else { + nr_of_ff_bytes = 0; + ff_flag = false; + } + + if (nr_of_ff_bytes >= 3) + ff_flag = true; + + response += (char) c; + if (recv_flag) { + if (response.find(0x05) != std::string::npos) { + exit_flag = true; + } + } + App.feed_wdt(); + delay(1); + + if (exit_flag || ff_flag) { + break; } } - return false; + if (ff_flag) + response = response.substr(0, response.length() - 3); // Remove last 3 0xFF + + ret = response.length(); + return ret; } -void Nextion::loop() { - while (this->available() >= 4) { - this->read_until_ack_(); + +/** + * @brief + * + * @param variable_name Name for the queue + */ +void Nextion::add_no_result_to_queue_(const std::string &variable_name) { + auto nextion_queue = make_unique(); + + nextion_queue->component = make_unique(); + nextion_queue->component->set_variable_name(variable_name); + + nextion_queue->queue_time = millis(); + + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); + + this->nextion_queue_.push_back(std::move(nextion_queue)); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param command + */ +void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { + if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty()) + return; + + if (this->send_command_(command)) { + this->add_no_result_to_queue_(variable_name); } } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { - this->send_command_printf("rtc0=%u", time.year); - this->send_command_printf("rtc1=%u", time.month); - this->send_command_printf("rtc2=%u", time.day_of_month); - this->send_command_printf("rtc3=%u", time.hour); - this->send_command_printf("rtc4=%u", time.minute); - this->send_command_printf("rtc5=%u", time.second); -} -#endif -void Nextion::set_backlight_brightness(uint8_t brightness) { this->send_command_printf("dim=%u", brightness); } -void Nextion::set_touch_sleep_timeout(uint16_t timeout) { this->send_command_printf("thsp=%u", timeout); } +bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, + ...) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return false; -void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } -void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + char buffer[256]; va_list arg; va_start(arg, format); - char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); va_end(arg); - if (ret > 0) - this->set_component_text(component, buffer); -} -void Nextion::set_wait_for_ack(bool wait_for_ack) { this->wait_for_ack_ = wait_for_ack; } + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } -void NextionTouchComponent::process(uint8_t page_id, uint8_t component_id, bool on) { - if (this->page_id_ == page_id && this->component_id_ == component_id) { - this->publish_state(on); + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief Sends a formatted command to the nextion + * + * @param variable_name Variable name for the queue + * @param format The printf-style command format, like "vis %s,0" + * @param ... The format arguments + */ +bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + + char buffer[256]; + va_list arg; + va_start(arg, format); + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } + + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value Value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ + +void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, int 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) { + 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, + 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(), + state_value); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value String value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ +void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, + const std::string &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, + const std::string &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, + const std::string &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_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(), + state_value.c_str()); +} + +void Nextion::add_to_get_queue(std::shared_ptr component) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return; + + auto nextion_queue = make_unique(); + + nextion_queue->component = component; + nextion_queue->queue_time = millis(); + + ESP_LOGN(TAG, "Add to queue type: %s component %s", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + std::string command = "get " + component->get_variable_name_to_send(); + + if (this->send_command_(command)) { + this->nextion_queue_.push_back(std::move(nextion_queue)); } } +/** + * @brief Add addt command to the queue + * + * @param component_id The waveform component id + * @param wave_chan_id The waveform channel to send it to + * @param buffer_to_send The buffer size + * @param buffer_size The buffer data + */ +void Nextion::add_addt_command_to_queue(std::shared_ptr component) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + auto nextion_queue = make_unique(); + + nextion_queue->component = std::make_shared(); + nextion_queue->queue_time = millis(); + + 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(std::move(nextion_queue)); + } +} + +void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } + +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"); } + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index a55ff747ee..1bee41f6cf 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,9 +1,21 @@ #pragma once -#include "esphome/core/component.h" +#include #include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" -#include "esphome/components/binary_sensor/binary_sensor.h" +#include "nextion_base.h" +#include "nextion_component.h" +#include "esphome/components/display/display_color_utils.h" + +#ifdef USE_NEXTION_TFT_UPLOAD +#ifdef USE_ESP32 +#include +#endif +#ifdef USE_ESP8266 +#include +#include +#endif +#endif #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -12,12 +24,14 @@ namespace esphome { namespace nextion { -class NextionTouchComponent; class Nextion; +class NextionComponentBase; using nextion_writer_t = std::function; -class Nextion : public PollingComponent, public uart::UARTDevice { +static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; + +class Nextion : public NextionBase, public PollingComponent, public uart::UARTDevice { public: /** * Set the text of a component to a static string. @@ -73,9 +87,20 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * 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) { - this->send_command_printf("%s.val=%s", component, picture); - } + void set_component_picture(const char *component, const char *picture); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as a uint32_t). + * + * Example: + * ```cpp + * it.set_component_background_color("button", 0xFF0000); + * ``` + * + * This will change the background color of the component `button` to red. + */ + void set_component_background_color(const char *component, uint32_t color); /** * Set the background color of a component. * @param component The component name. @@ -83,7 +108,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_background_color("button", "17013"); + * it.set_component_background_color("button", "RED"); * ``` * * This will change the background color of the component `button` to blue. @@ -91,6 +116,33 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_background_color(const char *component, const char *color); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_background_color("button", color); + * ``` + * + * This will change the background color of the component `button` to what color contains. + */ + 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). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", 0xFF0000 ); + * ``` + * + * 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, uint32_t color); /** * Set the pressed background color of a component. * @param component The component name. @@ -98,7 +150,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_background_color("button", "17013"); + * 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 @@ -107,6 +159,63 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ 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. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", color); + * ``` + * + * 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. + */ + void set_component_pressed_background_color(const char *component, Color color) override; + + /** + * Set the picture id of a component. + * @param component The component name. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_pic("textview", 1); + * ``` + * + * 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. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_picc("textview", 1); + * ``` + * + * This will change the background picture id of the component `textview`. + */ + void set_component_picc(const char *component, uint8_t pic_id); + + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as a uint32_t ). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", 0xFF0000); + * ``` + * + * This will change the font color of the component `textview` to a red color. + */ + void set_component_font_color(const char *component, uint32_t color); /** * Set the font color of a component. * @param component The component name. @@ -114,7 +223,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_font_color("textview", "17013"); + * it.set_component_font_color("textview", "RED"); * ``` * * This will change the font color of the component `textview` to a blue color. @@ -122,6 +231,34 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_font_color(const char *component, const char *color); + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", color); + * ``` + * + * 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. + */ + 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). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", 0xFF0000); + * ``` + * + * This will change the pressed font color of the component `button` to a red. + */ + void set_component_pressed_font_color(const char *component, uint32_t color); /** * Set the pressed font color of a component. * @param component The component name. @@ -129,7 +266,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_font_color("button", "17013"); + * it.set_component_pressed_font_color("button", "RED"); * ``` * * This will change the pressed font color of the component `button` to a blue color. @@ -137,6 +274,21 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ 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. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", color); + * ``` + * + * 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. + */ + 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. @@ -163,7 +315,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * 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); + void set_component_font(const char *component, uint8_t font_id) override; #ifdef USE_TIME /** * Send the current time to the nextion display. @@ -195,7 +347,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Hides the component named `button`. */ - void hide_component(const char *component); + void hide_component(const char *component) override; /** * Show a component. * @param component The component name. @@ -207,7 +359,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Shows the component named `button`. */ - void show_component(const char *component); + void show_component(const char *component) override; /** * Enable touch for a component. * @param component The component name. @@ -239,6 +391,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @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); /** * Display a picture at coordinates. * @param picture_id The picture id. @@ -263,14 +416,32 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * fill_area(50, 50, 100, 100, "17013"); + * fill_area(50, 50, 100, 100, "RED"); * ``` * - * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * 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 */ void fill_area(int x1, int y1, int width, int height, const char *color); + /** + * 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 (as Color). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, color); + * ``` + * + * 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 + */ + void fill_area(int x1, int y1, int width, int height, Color color); /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -290,6 +461,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void line(int x1, int y1, int x2, int y2, const char *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 (as Color). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, "17013"); + * ``` + * + * 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. + */ + void line(int x1, int y1, int x2, int y2, Color color); /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -309,6 +499,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void rectangle(int x1, int y1, int width, int height, const char *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 (as Color). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, "17013"); + * ``` + * + * 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. + */ + void rectangle(int x1, int y1, int width, int height, Color color); /** * Draw a circle outline * @param center_x The center x coordinate. @@ -317,6 +526,14 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @param color The color to draw with (as a string). */ void circle(int center_x, int center_y, int radius, const char *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 Color). + */ + void circle(int center_x, int center_y, int radius, Color color); /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -329,24 +546,41 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * it.filled_cricle(25, 25, 10, "17013"); * ``` * - * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of 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. */ void filled_circle(int center_x, int center_y, int radius, const char *color); - - /** Set the brightness of the backlight. - * - * @param brightness The brightness, from 0 to 100. + /** + * 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 (as Color). * * Example: * ```cpp - * it.set_backlight_brightness(30); + * it.filled_cricle(25, 25, 10, color); + * ``` + * + * 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. + */ + void filled_circle(int center_x, int center_y, int radius, Color color); + + /** Set the brightness of the backlight. + * + * @param brightness The brightness percentage from 0 to 1.0. + * + * Example: + * ```cpp + * it.set_backlight_brightness(.3); * ``` * * Changes the brightness of the display to 30%. */ - void set_backlight_brightness(uint8_t brightness); + void set_backlight_brightness(float brightness); /** * Set the touch sleep timeout of the display. * @param timeout Timeout in seconds. @@ -360,10 +594,46 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * `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 + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_wake_up_page(2); + * ``` + * + * The display will wake up to page 2. + */ + void set_wake_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, + * the first touch will only trigger the auto wake mode and not trigger a Touch Event. + * + * Example: + * ```cpp + * it.set_auto_wake_on_touch(true); + * ``` + * + * The display will wake up by touch. + */ + void set_auto_wake_on_touch(bool auto_wake); + /** + * 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); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } + void register_touch_component(NextionComponentBase *obj) { this->touch_.push_back(obj); } + void register_switch_component(NextionComponentBase *obj) { this->switchtype_.push_back(obj); } + void register_binarysensor_component(NextionComponentBase *obj) { this->binarysensortype_.push_back(obj); } + void register_sensor_component(NextionComponentBase *obj) { this->sensortype_.push_back(obj); } + void register_textsensor_component(NextionComponentBase *obj) { this->textsensortype_.push_back(obj); } + void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; @@ -371,11 +641,9 @@ class Nextion : public PollingComponent, public uart::UARTDevice { void loop() override; void set_writer(const nextion_writer_t &writer); - /** - * Manually send a raw command to the display and don't wait for an acknowledgement packet. - * @param command The command to write, for example "vis b0,0". - */ - void send_command_no_ack(const char *command); + // This function has been deprecated + void set_wait_for_ack(bool wait_for_ack); + /** * Manually send a raw formatted command to the display. * @param format The printf-style command format, like "vis %s,0" @@ -384,28 +652,197 @@ class Nextion : public PollingComponent, public uart::UARTDevice { */ bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); - void set_wait_for_ack(bool wait_for_ack); +#ifdef USE_NEXTION_TFT_UPLOAD + /** + * Set the tft file URL. https seems problamtic 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 + */ + void upload_tft(); + void dump_config() override; + + /** + * Softreset the Nextion + */ + void soft_reset(); + + /** Add a callback to be notified of sleep state changes. + * + * @param callback The void() callback. + */ + void add_sleep_state_callback(std::function &&callback); + + /** Add a callback to be notified of wake state changes. + * + * @param callback The void() callback. + */ + void add_wake_state_callback(std::function &&callback); + + /** Add a callback to be notified when the nextion completes its initialize setup. + * + * @param callback The void() callback. + */ + void add_setup_state_callback(std::function &&callback); + + void update_all_components(); + + /** + * @brief Set the nextion sensor state object. + * + * @param[in] queue_type + * Index of NextionQueueType. + * + * @param[in] name + * Component/variable name. + * + * @param[in] state + * State to set. + */ + void set_nextion_sensor_state(int queue_type, const std::string &name, float state); + 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(std::shared_ptr component, int 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; + + void add_no_result_to_queue_with_set(std::shared_ptr 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, + const std::string &state_value) override; + + void add_to_get_queue(std::shared_ptr component) override; + + void add_addt_command_to_queue(std::shared_ptr component) override; + + void update_components_by_prefix(const std::string &prefix); + + void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) { + 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_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } protected: - bool ack_(); - bool read_until_ack_(); + std::deque> nextion_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_; + + void process_nextion_commands_(); + void process_serial_(); + bool is_updating_ = false; + uint32_t touch_sleep_timeout_ = 0; + int wake_up_page_ = -1; + bool auto_wake_on_touch_ = true; + + /** + * Manually send a raw command to the display and don't wait for an acknowledgement packet. + * @param command The command to write, for example "vis b0,0". + */ + bool send_command_(const std::string &command); + void add_no_result_to_queue_(const std::string &variable_name); + bool add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) + __attribute__((format(printf, 3, 4))); + void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); + + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) + __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, + 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); + +#ifdef USE_NEXTION_TFT_UPLOAD +#ifdef USE_ESP8266 + WiFiClient *wifi_client_{nullptr}; + BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; + WiFiClient *get_wifi_client_(); +#endif + + /** + * 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. + */ + 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); + + /** + * start update tft file to nextion. + * + * @param const uint8_t *file_buf + * @param size_t buf_size + * @return true if success, false for failure. + */ + bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size); + void upload_end_(); + +#endif // USE_NEXTION_TFT_UPLOAD + + bool get_is_connected_() { return this->is_connected_; } + + bool check_connect_(); + + std::vector touch_; + std::vector switchtype_; + std::vector sensortype_; + std::vector textsensortype_; + std::vector binarysensortype_; + CallbackManager setup_callback_{}; + CallbackManager sleep_callback_{}; + CallbackManager wake_callback_{}; - std::vector touch_; optional writer_; - bool wait_for_ack_{true}; float brightness_{1.0}; + + std::string device_model_; + std::string firmware_version_; + std::string serial_number_; + std::string flash_size_; + + 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 + 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; + uint32_t started_ms_ = 0; + bool sent_setup_commands_ = false; }; - -class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { - public: - void set_page_id(uint8_t page_id) { page_id_ = page_id; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } - void process(uint8_t page_id, uint8_t component_id, bool on); - - protected: - uint8_t page_id_; - uint8_t component_id_; -}; - } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h new file mode 100644 index 0000000000..d91c70c960 --- /dev/null +++ b/esphome/components/nextion/nextion_base.h @@ -0,0 +1,59 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_component_base.h" +namespace esphome { +namespace nextion { + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define NEXTION_PROTOCOL_LOG +#endif + +#ifdef NEXTION_PROTOCOL_LOG +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define ESP_LOGN(tag, ...) ESP_LOGVV(tag, __VA_ARGS__) +#else +#define ESP_LOGN(tag, ...) ESP_LOGD(tag, __VA_ARGS__) +#endif +#else +#define ESP_LOGN(tag, ...) \ + {} +#endif + +class NextionBase; + +class NextionBase { + public: + virtual void add_no_result_to_queue_with_set(std::shared_ptr component, int 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; + + virtual void add_no_result_to_queue_with_set(std::shared_ptr component, + const std::string &state_value) = 0; + virtual void add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value) = 0; + + virtual void add_addt_command_to_queue(std::shared_ptr component) = 0; + + virtual void add_to_get_queue(std::shared_ptr component) = 0; + + 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_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; + + virtual void show_component(const char *component) = 0; + virtual void hide_component(const char *component) = 0; + + bool is_sleeping() { return this->is_sleeping_; } + bool is_setup() { return this->is_setup_; } + + protected: + bool is_setup_ = false; + bool is_sleeping_ = false; +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp new file mode 100644 index 0000000000..931b934ba2 --- /dev/null +++ b/esphome/components/nextion/nextion_commands.cpp @@ -0,0 +1,234 @@ +#include "nextion.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion"; + +// Sleep safe commands +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_touch_sleep_timeout(uint16_t timeout) { + if (timeout < 3 || timeout > 65535) { + ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); + return; + } + + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true); +} + +void Nextion::sleep(bool sleep) { + if (sleep) { // Set sleep + this->is_sleeping_ = true; + this->add_no_result_to_queue_with_set_internal_("sleep", "sleep", 1, true); + } else { // Turn off sleep. Wait for a sleep_wake return before setting sleep off + this->add_no_result_to_queue_with_set_internal_("sleep_wake", "sleep", 0, true); + } +} +// 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); +} + +void Nextion::set_component_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); +} + +void Nextion::set_component_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, + 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); +} + +void Nextion::set_component_pressed_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); +} + +void Nextion::set_component_pressed_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, + 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); +} + +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_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_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); +} + +void Nextion::set_component_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, + 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); +} + +void Nextion::set_component_pressed_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[256]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + this->set_component_text(component, buffer); +} + +// General Nextion +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } + +void Nextion::set_backlight_brightness(float brightness) { + if (brightness < 0 || brightness > 1.0) { + ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0"); + return; + } + this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast(brightness * 100)); +} + +void Nextion::set_auto_wake_on_touch(bool auto_wake) { + this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); +} + +// 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); +} + +void Nextion::hide_component(const char *component) { + this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); +} + +void Nextion::show_component(const char *component) { + this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); +} + +void Nextion::enable_component_touch(const char *component) { + this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); +} + +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_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::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::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::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); +} + +// 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::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(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, + 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::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::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::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::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::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(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::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)); +} + +#ifdef USE_TIME +void Nextion::set_nextion_rtc_time(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); + this->add_no_result_to_queue_with_printf_("rtc3", "rtc3=%u", time.hour); + 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 new file mode 100644 index 0000000000..bbb2cf6cb2 --- /dev/null +++ b/esphome/components/nextion/nextion_component.cpp @@ -0,0 +1,116 @@ +#include "nextion_component.h" + +namespace esphome { +namespace nextion { + +void NextionComponent::set_background_color(Color bco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->bco_ = bco; + this->bco_needs_update_ = true; + this->bco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_background_pressed_color(Color bco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + + this->bco2_ = bco2; + this->bco2_needs_update_ = true; + this->bco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_color(Color pco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco_ = pco; + this->pco_needs_update_ = true; + this->pco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_pressed_color(Color pco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco2_ = pco2; + this->pco2_needs_update_ = true; + this->pco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_font_id(uint8_t font_id) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->font_id_ = font_id; + this->font_id_needs_update_ = true; + this->font_id_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_visible(bool visible) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->visible_ = visible; + this->visible_needs_update_ = true; + this->visible_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::update_component_settings(bool force_update) { + if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || + (!this->visible_needs_update_ && !this->visible_)) { + this->needs_to_send_update_ = true; + return; + } + + if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) { + std::string name_to_send = this->variable_name_; + + size_t pos = name_to_send.find_last_of('.'); + if (pos != std::string::npos) { + name_to_send = name_to_send.substr(pos + 1); + } + + this->visible_needs_update_ = false; + + if (this->visible_) { + this->nextion_->show_component(name_to_send.c_str()); + this->send_state_to_nextion(); + } else { + this->nextion_->hide_component(name_to_send.c_str()); + return; + } + } + + if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); + this->bco_needs_update_ = false; + } + if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_); + 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->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->pco2_needs_update_ = false; + } + + if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) { + this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_); + this->font_id_needs_update_ = false; + } +} +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h new file mode 100644 index 0000000000..2f3c4f3c16 --- /dev/null +++ b/esphome/components/nextion/nextion_component.h @@ -0,0 +1,49 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionComponent; + +class NextionComponent : public NextionComponentBase { + public: + void update_component_settings() override { this->update_component_settings(false); }; + + void update_component_settings(bool force_update) override; + + void set_background_color(Color bco); + void set_background_pressed_color(Color bco2); + void set_foreground_color(Color pco); + void set_foreground_pressed_color(Color pco2); + void set_font_id(uint8_t font_id); + void set_visible(bool visible); + + protected: + NextionBase *nextion_; + + bool bco_needs_update_ = false; + bool bco_is_set_ = false; + Color bco_; + bool bco2_needs_update_ = false; + bool bco2_is_set_ = false; + Color bco2_; + bool pco_needs_update_ = false; + bool pco_is_set_ = false; + Color pco_; + bool pco2_needs_update_ = false; + bool pco2_is_set_ = false; + Color pco2_; + uint8_t font_id_ = 0; + bool font_id_needs_update_ = false; + bool font_id_is_set_ = false; + + bool visible_ = true; + bool visible_needs_update_ = false; + bool visible_is_set_ = false; + + // void send_state_to_nextion() = 0; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h new file mode 100644 index 0000000000..2725d5a30c --- /dev/null +++ b/esphome/components/nextion/nextion_component_base.h @@ -0,0 +1,96 @@ +#pragma once +#include +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace nextion { + +enum NextionQueueType { + NO_RESULT = 0, + SENSOR = 1, + BINARY_SENSOR = 2, + SWITCH = 3, + TEXT_SENSOR = 4, + WAVEFORM_SENSOR = 5, +}; + +static const char *const NEXTION_QUEUE_TYPE_STRINGS[] = {"NO_RESULT", "SENSOR", "BINARY_SENSOR", + "SWITCH", "TEXT_SENSOR", "WAVEFORM_SENSOR"}; + +class NextionComponentBase; + +class NextionQueue { + public: + virtual ~NextionQueue() = default; + std::shared_ptr component; + uint32_t queue_time = 0; +}; + +class NextionComponentBase { + public: + virtual ~NextionComponentBase() = default; + + void set_variable_name(const std::string &variable_name, const std::string &variable_name_to_send = "") { + variable_name_ = variable_name; + if (variable_name_to_send.empty()) { + variable_name_to_send_ = variable_name; + } else { + variable_name_to_send_ = variable_name_to_send; + } + } + + virtual void update_component_settings(){}; + virtual void update_component_settings(bool force_update){}; + + virtual void update_component(){}; + virtual void process_sensor(const std::string &variable_name, int state){}; + virtual void process_touch(uint8_t page_id, uint8_t component_id, bool on){}; + virtual void process_text(const std::string &variable_name, const std::string &text_value){}; + virtual void process_bool(const std::string &variable_name, bool on){}; + + virtual void set_state(float state){}; + virtual void set_state(float state, bool publish){}; + virtual void set_state(float state, bool publish, bool send_to_nextion){}; + + virtual void set_state(bool state){}; + virtual void set_state(bool state, bool publish){}; + virtual void set_state(bool state, bool publish, bool send_to_nextion){}; + + virtual void set_state(const std::string &state) {} + virtual void set_state(const std::string &state, bool publish) {} + virtual void set_state(const std::string &state, bool publish, bool send_to_nextion){}; + + uint8_t get_component_id() { return this->component_id_; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + uint8_t get_wave_channel_id() { return this->wave_chan_id_; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + + std::vector get_wave_buffer() { return this->wave_buffer_; } + size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + + std::string get_variable_name() { return this->variable_name_; } + std::string get_variable_name_to_send() { return this->variable_name_to_send_; } + virtual NextionQueueType get_queue_type() { return NextionQueueType::NO_RESULT; } + virtual std::string get_queue_type_string() { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } + virtual void set_state_from_int(int state_value, bool publish, bool send_to_nextion){}; + virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; + virtual void send_state_to_nextion(){}; + bool get_needs_to_send_update() { return this->needs_to_send_update_; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + + protected: + std::string variable_name_; + std::string variable_name_to_send_; + + uint8_t component_id_ = 0; + uint8_t wave_chan_id_ = UINT8_MAX; + std::vector wave_buffer_; + int wave_max_length_ = 255; + + bool needs_to_send_update_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..cebdbec31a --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,349 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD + +#include "esphome/core/application.h" +#include "esphome/core/macros.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" +#include "esphome/components/network/util.h" + +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 ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) + http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); +#elif ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) + http->setFollowRedirects(true); +#endif +#if 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 (uint32_t 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 ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); +#elif ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) + http.setFollowRedirects(true); +#endif +#if 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"); +#ifdef USE_ESP32 + if (psramFound()) + free(this->transfer_buffer_); // NOLINT + else +#endif + delete this->transfer_buffer_; + 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 (int 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 (psramFound()) { + 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) { +#ifdef USE_ESP32 + if (psramFound()) { + ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); + this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); + if (this->transfer_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); + this->upload_end_(); + } + } else { +#endif + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) + 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_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) + + if (!this->transfer_buffer_) + this->upload_end_(); +#ifdef USE_ESP32 + } +#endif + } + + 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) { + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); // NOLINT(cppcoreguidelines-owning-memory) + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + this->wifi_client_ = new WiFiClient(); // NOLINT(cppcoreguidelines-owning-memory) + } + return this->wifi_client_; +} +#endif +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py new file mode 100644 index 0000000000..8a32adc1f6 --- /dev/null +++ b/esphome/components/nextion/sensor/__init__.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor + +from esphome.const import ( + CONF_ID, + CONF_COMPONENT_ID, +) +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_SENSOR_COMPONENT_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, + CONF_PRECISION, + CONF_WAVE_CHANNEL_ID, + CONF_WAVE_MAX_VALUE, + CONF_WAVEFORM_SEND_LAST_VALUE, + CONF_WAVE_MAX_LENGTH, +) + + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent) + + +def CheckWaveID(value): + value = cv.int_(value) + if value < 0 or value > 3: + raise cv.Invalid(f"Valid range for {CONF_WAVE_CHANNEL_ID} is 0-3") + return value + + +def _validate(config): + if CONF_WAVE_CHANNEL_ID in config and CONF_COMPONENT_ID not in config: + raise cv.Invalid( + f"{CONF_COMPONENT_ID} is required when {CONF_WAVE_CHANNEL_ID} is set" + ) + + return config + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + accuracy_decimals=2, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(NextionSensor), + cv.Optional(CONF_PRECISION, default=0): cv.int_range(min=0, max=8), + cv.Optional(CONF_WAVE_CHANNEL_ID): CheckWaveID, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + cv.Optional(CONF_WAVE_MAX_LENGTH, default=255): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVE_MAX_VALUE, default=100): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVEFORM_SEND_LAST_VALUE, default=True): cv.boolean, + } + ) + .extend(CONFIG_SENSOR_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_ID, CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), + _validate, +) + + +async def to_code(config): + + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(hub.register_sensor_component(var)) + + await setup_component_core_(var, config, ".val") + + if CONF_PRECISION in config: + cg.add(var.set_precision(config[CONF_PRECISION])) + + if CONF_COMPONENT_ID in config: + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + + if CONF_WAVE_CHANNEL_ID in config: + cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID])) + + if CONF_WAVEFORM_SEND_LAST_VALUE in config: + cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])) + + if CONF_WAVE_MAX_VALUE in config: + cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) + + if CONF_WAVE_MAX_LENGTH in config: + cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp new file mode 100644 index 0000000000..e983ebcc6f --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -0,0 +1,110 @@ +#include "nextion_sensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_sensor"; + +void NextionSensor::process_sensor(const std::string &variable_name, int state) { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed sensor \"%s\" state %d", variable_name.c_str(), state); + } +} + +void NextionSensor::add_to_wave_buffer(float state) { + this->needs_to_send_update_ = true; + + int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100); + + wave_buffer_.push_back(wave_state); + + if (this->wave_buffer_.size() > this->wave_max_length_) { + this->wave_buffer_.erase(this->wave_buffer_.begin()); + } +} + +void NextionSensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + this->nextion_->add_to_get_queue(shared_from_this()); + } else { + if (this->send_last_value_) { + this->add_to_wave_buffer(this->last_value_); + } + + this->wave_update_(); + } +} + +void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (std::isnan(state)) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + + if (this->precision_ > 0) { + double to_multiply = pow(10, this->precision_); + int state_value = (int) (state * to_multiply); + + this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + } + } + } + } else { + if (this->send_last_value_) { + this->last_value_ = state; // Update will handle setting the buffer + } else { + this->add_to_wave_buffer(state); + } + } + + if (this->wave_chan_id_ == UINT8_MAX) { + if (publish) { + this->publish_state(state); + } else { + this->raw_state = state; + this->state = state; + this->has_state_ = true; + } + } + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %lf", this->variable_name_.c_str(), state); +} + +void NextionSensor::wave_update_() { + if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) { + return; + } + +#ifdef NEXTION_PROTOCOL_LOG + size_t buffer_to_send = + this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255 + + ESP_LOGN(TAG, "wave_update send %zu of %zu value(s) to wave nextion component id %d and wave channel id %d", + buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); +#endif + + this->nextion_->add_addt_command_to_queue(shared_from_this()); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h new file mode 100644 index 0000000000..068ff0451b --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -0,0 +1,52 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSensor; + +class NextionSensor : public NextionComponent, + public sensor::Sensor, + public PollingComponent, + public std::enable_shared_from_this { + public: + NextionSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + + void update_component() override { this->update(); } + void update() override; + void add_to_wave_buffer(float state); + void set_precision(uint8_t precision) { this->precision_ = precision; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } + void process_sensor(const std::string &variable_name, int state) override; + + void set_state(float state) override { this->set_state(state, true, true); } + void set_state(float state, bool publish) override { this->set_state(state, publish, true); } + void set_state(float state, bool publish, bool send_to_nextion) override; + + void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + NextionQueueType get_queue_type() override { + return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; + } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } + + protected: + uint8_t precision_ = 0; + uint32_t wave_maxvalue_ = 255; + + float last_value_ = 0; + bool send_last_value_ = true; + void wave_update_(); +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py new file mode 100644 index 0000000000..068681fa14 --- /dev/null +++ b/esphome/components/nextion/switch/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch + +from esphome.const import CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + CONFIG_SWITCH_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent) + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionSwitch), + } + ) + .extend(CONFIG_SWITCH_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + cg.add(hub.register_switch_component(var)) + + await setup_component_core_(var, config, ".val") diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp new file mode 100644 index 0000000000..0bd958e0d8 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -0,0 +1,52 @@ +#include "nextion_switch.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_switch"; + +void NextionSwitch::process_bool(const std::string &variable_name, bool on) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(on); + + ESP_LOGD(TAG, "Processed switch \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionSwitch::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(shared_from_this()); +} + +void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + } + } + if (publish) { + this->publish_state(state); + } else { + this->state = state; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Updated switch \"%s\" state %s", this->variable_name_.c_str(), ONOFF(state)); +} + +void NextionSwitch::write_state(bool state) { this->set_state(state); } + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h new file mode 100644 index 0000000000..d7783e5c51 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -0,0 +1,37 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSwitch; + +class NextionSwitch : public NextionComponent, + public switch_::Switch, + public PollingComponent, + public std::enable_shared_from_this { + public: + NextionSwitch(NextionBase *nextion) { this->nextion_ = nextion; } + + void update() override; + void update_component() override { this->update(); } + void process_bool(const std::string &variable_name, bool on) override; + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::SWITCH; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + void write_state(bool state) override; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py new file mode 100644 index 0000000000..9c170dd807 --- /dev/null +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID + +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_TEXT_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionTextSensor = nextion_ns.class_( + "NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionTextSensor), + } + ) + .extend(CONFIG_TEXT_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + cg.add(hub.register_textsensor_component(var)) + + await setup_component_core_(var, config, ".txt") diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp new file mode 100644 index 0000000000..fa7cb35025 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -0,0 +1,49 @@ +#include "nextion_textsensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion_textsensor"; + +void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(text_value); + ESP_LOGD(TAG, "Processed text_sensor \"%s\" state \"%s\"", variable_name.c_str(), text_value.c_str()); + } +} + +void NextionTextSensor::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(shared_from_this()); +} + +void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for text_sensor \"%s\" state \"%s\"", this->variable_name_.c_str(), state.c_str()); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h new file mode 100644 index 0000000000..762797727d --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -0,0 +1,35 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionTextSensor; + +class NextionTextSensor : public NextionComponent, + public text_sensor::TextSensor, + public PollingComponent, + public std::enable_shared_from_this { + public: + NextionTextSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void update() override; + void update_component() override { this->update(); } + void on_state_changed(const std::string &state); + + void process_text(const std::string &variable_name, const std::string &text_value) override; + + void set_state(const std::string &state, bool publish) override { this->set_state(state, publish, true); } + void set_state(const std::string &state) override { this->set_state(state, true, true); } + void set_state(const std::string &state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::TEXT_SENSOR; } + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index 4e295d4469..d8c940254e 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -10,16 +10,13 @@ NdefMessage::NdefMessage(std::vector &data) { uint8_t index = 0; while (index <= data.size()) { uint8_t tnf_byte = data[index++]; - bool me = tnf_byte & 0x40; - bool sr = tnf_byte & 0x10; - bool il = tnf_byte & 0x08; - uint8_t tnf = tnf_byte & 0x07; + bool me = tnf_byte & 0x40; // Message End bit (is set if this is the last record of the message) + bool sr = tnf_byte & 0x10; // Short record bit (is set if payload size is less or equal to 255 bytes) + bool il = tnf_byte & 0x08; // ID length bit (is set if ID Length field exists) + uint8_t tnf = tnf_byte & 0x07; // Type Name Format ESP_LOGVV(TAG, "me=%s, sr=%s, il=%s, tnf=%d", YESNO(me), YESNO(sr), YESNO(il), tnf); - auto record = new NdefRecord(); - record->set_tnf(tnf); - uint8_t type_length = data[index++]; uint32_t payload_length = 0; if (sr) { @@ -38,59 +35,60 @@ NdefMessage::NdefMessage(std::vector &data) { ESP_LOGVV(TAG, "Lengths: type=%d, payload=%d, id=%d", type_length, payload_length, id_length); std::string type_str(data.begin() + index, data.begin() + index + type_length); - record->set_type(type_str); + index += type_length; + std::string id_str = ""; if (il) { - std::string id_str(data.begin() + index, data.begin() + index + id_length); - record->set_id(id_str); + id_str = std::string(data.begin() + index, data.begin() + index + id_length); index += id_length; } - uint8_t payload_identifier = 0x00; - if (type_str == "U") { - payload_identifier = data[index++]; - payload_length -= 1; + std::vector payload_data(data.begin() + index, data.begin() + index + payload_length); + + std::unique_ptr record; + + // Based on tnf and type, create a more specific NdefRecord object + // constructed from the payload data + if (tnf == TNF_WELL_KNOWN && type_str == "U") { + record = make_unique(payload_data); + } else if (tnf == TNF_WELL_KNOWN && type_str == "T") { + record = make_unique(payload_data); + } else { + // Could not recognize the record, so store as generic one. + record = make_unique(payload_data); + record->set_tnf(tnf); + record->set_type(type_str); } - std::string payload_str(data.begin() + index, data.begin() + index + payload_length); + record->set_id(id_str); - if (payload_identifier > 0x00 && payload_identifier <= PAYLOAD_IDENTIFIERS_COUNT) { - payload_str.insert(0, PAYLOAD_IDENTIFIERS[payload_identifier]); - } - - record->set_payload(payload_str); index += payload_length; - this->add_record(record); ESP_LOGV(TAG, "Adding record type %s = %s", record->get_type().c_str(), record->get_payload().c_str()); + this->add_record(std::move(record)); if (me) break; } } -bool NdefMessage::add_record(NdefRecord *record) { +bool NdefMessage::add_record(std::unique_ptr record) { if (this->records_.size() >= MAX_NDEF_RECORDS) { ESP_LOGE(TAG, "Too many records. Max: %d", MAX_NDEF_RECORDS); return false; } - this->records_.push_back(record); + this->records_.emplace_back(std::move(record)); return true; } bool NdefMessage::add_text_record(const std::string &text) { return this->add_text_record(text, "en"); }; bool NdefMessage::add_text_record(const std::string &text, const std::string &encoding) { - std::string payload = to_string(text.length()) + encoding + text; - auto r = new NdefRecord(TNF_WELL_KNOWN, "T", payload); - return this->add_record(r); + return this->add_record(make_unique(encoding, text)); } -bool NdefMessage::add_uri_record(const std::string &uri) { - auto r = new NdefRecord(TNF_WELL_KNOWN, "U", uri); - return this->add_record(r); -} +bool NdefMessage::add_uri_record(const std::string &uri) { return this->add_record(make_unique(uri)); } std::vector NdefMessage::encode() { std::vector data; diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index e47a7b992a..5e44a06011 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -1,8 +1,12 @@ #pragma once +#include + #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "ndef_record.h" +#include "ndef_record_text.h" +#include "ndef_record_uri.h" namespace esphome { namespace nfc { @@ -11,12 +15,18 @@ static const uint8_t MAX_NDEF_RECORDS = 4; class NdefMessage { public: - NdefMessage(){}; + NdefMessage() = default; NdefMessage(std::vector &data); + NdefMessage(const NdefMessage &msg) { + records_.reserve(msg.records_.size()); + for (const auto &r : msg.records_) { + records_.emplace_back(r->clone()); + } + } - std::vector get_records() { return this->records_; }; + const std::vector> &get_records() { return this->records_; }; - bool add_record(NdefRecord *record); + bool add_record(std::unique_ptr record); bool add_text_record(const std::string &text); bool add_text_record(const std::string &text, const std::string &encoding); bool add_uri_record(const std::string &uri); @@ -24,7 +34,7 @@ class NdefMessage { std::vector encode(); protected: - std::vector records_; + std::vector> records_; }; } // namespace nfc diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp index a75f5978ec..8a3a7d375d 100644 --- a/esphome/components/nfc/ndef_record.cpp +++ b/esphome/components/nfc/ndef_record.cpp @@ -5,40 +5,22 @@ namespace nfc { static const char *const TAG = "nfc.ndef_record"; -uint32_t NdefRecord::get_encoded_size() { - uint32_t size = 2; - if (this->payload_.length() > 255) { - size += 4; - } else { - size += 1; - } - if (this->id_.length()) { - size += 1; - } - size += (this->type_.length() + this->payload_.length() + this->id_.length()); - return size; +NdefRecord::NdefRecord(std::vector payload_data) { + this->payload_ = std::string(payload_data.begin(), payload_data.end()); } std::vector NdefRecord::encode(bool first, bool last) { std::vector data; - data.push_back(this->get_tnf_byte(first, last)); + // Get encoded payload, this is overriden by more specific record classes + std::vector payload_data = get_encoded_payload(); + + size_t payload_length = payload_data.size(); + + data.push_back(this->create_flag_byte(first, last, payload_length)); data.push_back(this->type_.length()); - uint8_t payload_prefix = 0x00; - uint8_t payload_prefix_length = 0x00; - for (uint8_t i = 1; i < PAYLOAD_IDENTIFIERS_COUNT; i++) { - std::string prefix = PAYLOAD_IDENTIFIERS[i]; - if (this->payload_.substr(0, prefix.length()).find(prefix) != std::string::npos) { - payload_prefix = i; - payload_prefix_length = prefix.length(); - break; - } - } - - uint32_t payload_length = this->payload_.length() - payload_prefix_length + 1; - if (payload_length <= 255) { data.push_back(payload_length); } else { @@ -58,25 +40,23 @@ std::vector NdefRecord::encode(bool first, bool last) { data.insert(data.end(), this->id_.begin(), this->id_.end()); } - data.push_back(payload_prefix); - - data.insert(data.end(), this->payload_.begin() + payload_prefix_length, this->payload_.end()); + data.insert(data.end(), payload_data.begin(), payload_data.end()); return data; } -uint8_t NdefRecord::get_tnf_byte(bool first, bool last) { - uint8_t value = this->tnf_; +uint8_t NdefRecord::create_flag_byte(bool first, bool last, size_t payload_size) { + uint8_t value = this->tnf_ & 0b00000111; if (first) { - value = value | 0x80; + value = value | 0x80; // Set MB bit } if (last) { - value = value | 0x40; + value = value | 0x40; // Set ME bit } - if (this->payload_.length() <= 255) { - value = value | 0x10; + if (payload_size <= 255) { + value = value | 0x10; // Set SR bit } if (this->id_.length()) { - value = value | 0x08; + value = value | 0x08; // Set IL bit } return value; }; diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 2059444882..4fab1c03e4 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -15,86 +15,40 @@ static const uint8_t TNF_UNKNOWN = 0x05; static const uint8_t TNF_UNCHANGED = 0x06; static const uint8_t TNF_RESERVED = 0x07; -static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; -static const char *const PAYLOAD_IDENTIFIERS[] = {"", - "http://www.", - "https://www.", - "http://", - "https://", - "tel:", - "mailto:", - "ftp://anonymous:anonymous@", - "ftp://ftp.", - "ftps://", - "sftp://", - "smb://", - "nfs://", - "ftp://", - "dav://", - "news:", - "telnet://", - "imap:", - "rtsp://", - "urn:", - "pop:", - "sip:", - "sips:", - "tftp:", - "btspp://", - "btl2cap://", - "btgoep://", - "tcpobex://", - "irdaobex://", - "file://", - "urn:epc:id:", - "urn:epc:tag:", - "urn:epc:pat:", - "urn:epc:raw:", - "urn:epc:", - "urn:nfc:"}; - class NdefRecord { public: NdefRecord(){}; - NdefRecord(uint8_t tnf, const std::string &type, const std::string &payload) { - this->tnf_ = tnf; - this->type_ = type; - this->set_payload(payload); - }; - NdefRecord(uint8_t tnf, const std::string &type, const std::string &payload, const std::string &id) { - this->tnf_ = tnf; - this->type_ = type; - this->set_payload(payload); - this->id_ = id; - }; - NdefRecord(const NdefRecord &rhs) { - this->tnf_ = rhs.tnf_; - this->type_ = rhs.type_; - this->payload_ = rhs.payload_; - this->payload_identifier_ = rhs.payload_identifier_; - this->id_ = rhs.id_; - }; + NdefRecord(std::vector payload_data); void set_tnf(uint8_t tnf) { this->tnf_ = tnf; }; void set_type(const std::string &type) { this->type_ = type; }; - void set_payload_identifier(uint8_t payload_identifier) { this->payload_identifier_ = payload_identifier; }; void set_payload(const std::string &payload) { this->payload_ = payload; }; void set_id(const std::string &id) { this->id_ = id; }; + NdefRecord(const NdefRecord &) = default; + virtual ~NdefRecord() {} + virtual std::unique_ptr clone() const { // To allow copying polymorphic classes + return make_unique(*this); + }; uint32_t get_encoded_size(); std::vector encode(bool first, bool last); - uint8_t get_tnf_byte(bool first, bool last); - const std::string &get_type() { return this->type_; }; - const std::string &get_id() { return this->id_; }; - const std::string &get_payload() { return this->payload_; }; + uint8_t create_flag_byte(bool first, bool last, size_t payload_size); + + const std::string &get_type() const { return this->type_; }; + const std::string &get_id() const { return this->id_; }; + virtual const std::string &get_payload() const { return this->payload_; }; + + virtual std::vector get_encoded_payload() { + std::vector empty_payload; + return empty_payload; + }; protected: uint8_t tnf_; std::string type_; - uint8_t payload_identifier_; - std::string payload_; std::string id_; + std::string payload_; }; } // namespace nfc diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp new file mode 100644 index 0000000000..80b0108b46 --- /dev/null +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -0,0 +1,40 @@ +#include "ndef_record_text.h" +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.ndef_record_text"; + +NdefRecordText::NdefRecordText(const std::vector &payload) { + if (payload.empty()) { + ESP_LOGE(TAG, "Record payload too short"); + return; + } + + uint8_t language_code_length = payload[0] & 0b00111111; // Todo, make use of encoding bit? + + this->language_code_ = std::string(payload.begin() + 1, payload.begin() + 1 + language_code_length); + + this->text_ = std::string(payload.begin() + 1 + language_code_length, payload.end()); + + this->tnf_ = TNF_WELL_KNOWN; + + this->type_ = "T"; +} + +std::vector NdefRecordText::get_encoded_payload() { + std::vector data; + + uint8_t flag_byte = this->language_code_.length() & 0b00111111; // UTF8 assumed + + data.push_back(flag_byte); + + data.insert(data.end(), this->language_code_.begin(), this->language_code_.end()); + + data.insert(data.end(), this->text_.begin(), this->text_.end()); + return data; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record_text.h b/esphome/components/nfc/ndef_record_text.h new file mode 100644 index 0000000000..94375cc860 --- /dev/null +++ b/esphome/components/nfc/ndef_record_text.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +class NdefRecordText : public NdefRecord { + public: + NdefRecordText(){}; + NdefRecordText(const std::vector &payload); + NdefRecordText(const std::string &language_code, const std::string &text) { + this->tnf_ = TNF_WELL_KNOWN; + this->type_ = "T"; + this->language_code_ = language_code; + this->text_ = text; + }; + NdefRecordText(const std::string &language_code, const std::string &text, const std::string &id) { + this->tnf_ = TNF_WELL_KNOWN; + this->type_ = "T"; + this->language_code_ = language_code; + this->text_ = text; + this->id_ = id; + }; + NdefRecordText(const NdefRecordText &) = default; + + std::unique_ptr clone() const override { return make_unique(*this); }; + + std::vector get_encoded_payload() override; + + const std::string &get_payload() const override { return this->text_; }; + + protected: + std::string text_; + std::string language_code_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record_uri.cpp b/esphome/components/nfc/ndef_record_uri.cpp new file mode 100644 index 0000000000..9064f04f29 --- /dev/null +++ b/esphome/components/nfc/ndef_record_uri.cpp @@ -0,0 +1,48 @@ +#include "ndef_record_uri.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.ndef_record_uri"; + +NdefRecordUri::NdefRecordUri(const std::vector &payload) { + if (payload.empty()) { + ESP_LOGE(TAG, "Record payload too short"); + return; + } + + uint8_t payload_identifier = payload[0]; // First byte of payload is prefix code + + std::string uri(payload.begin() + 1, payload.end()); + + if (payload_identifier > 0x00 && payload_identifier <= PAYLOAD_IDENTIFIERS_COUNT) { + uri.insert(0, PAYLOAD_IDENTIFIERS[payload_identifier]); + } + + this->tnf_ = TNF_WELL_KNOWN; + this->type_ = "U"; + this->set_uri(uri); +} + +std::vector NdefRecordUri::get_encoded_payload() { + std::vector data; + + uint8_t payload_prefix = 0x00; + uint8_t payload_prefix_length = 0x00; + for (uint8_t i = 1; i < PAYLOAD_IDENTIFIERS_COUNT; i++) { + std::string prefix = PAYLOAD_IDENTIFIERS[i]; + if (this->uri_.substr(0, prefix.length()).find(prefix) != std::string::npos) { + payload_prefix = i; + payload_prefix_length = prefix.length(); + break; + } + } + + data.push_back(payload_prefix); + + data.insert(data.end(), this->uri_.begin() + payload_prefix_length, this->uri_.end()); + return data; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h new file mode 100644 index 0000000000..4c21724c5c --- /dev/null +++ b/esphome/components/nfc/ndef_record_uri.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static const char *const PAYLOAD_IDENTIFIERS[] = {"", + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:"}; + +class NdefRecordUri : public NdefRecord { + public: + NdefRecordUri(){}; + NdefRecordUri(const std::vector &payload); + NdefRecordUri(const std::string &uri) { + this->tnf_ = TNF_WELL_KNOWN; + this->type_ = "U"; + this->uri_ = uri; + }; + NdefRecordUri(const std::string &uri, const std::string &id) { + this->tnf_ = TNF_WELL_KNOWN; + this->type_ = "U"; + this->uri_ = uri; + this->id_ = id; + }; + NdefRecordUri(const NdefRecordUri &) = default; + std::unique_ptr clone() const override { return make_unique(*this); }; + + void set_uri(const std::string &uri) { this->uri_ = uri; }; + + std::vector get_encoded_payload() override; + const std::string &get_payload() const override { return this->uri_; }; + + protected: + std::string uri_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 01f63e6ec9..706c09a5aa 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -1,4 +1,5 @@ #include "nfc.h" +#include #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/nfc/nfc_tag.h b/esphome/components/nfc/nfc_tag.h index ff0d1c39b4..2dfc431428 100644 --- a/esphome/components/nfc/nfc_tag.h +++ b/esphome/components/nfc/nfc_tag.h @@ -1,6 +1,9 @@ #pragma once +#include + #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include "ndef_message.h" namespace esphome { @@ -20,27 +23,33 @@ class NfcTag { this->uid_ = uid; this->tag_type_ = tag_type; }; - NfcTag(std::vector &uid, const std::string &tag_type, nfc::NdefMessage *ndef_message) { + NfcTag(std::vector &uid, const std::string &tag_type, std::unique_ptr ndef_message) { this->uid_ = uid; this->tag_type_ = tag_type; - this->ndef_message_ = ndef_message; + this->ndef_message_ = std::move(ndef_message); }; NfcTag(std::vector &uid, const std::string &tag_type, std::vector &ndef_data) { this->uid_ = uid; this->tag_type_ = tag_type; - this->ndef_message_ = new NdefMessage(ndef_data); + this->ndef_message_ = make_unique(ndef_data); }; + NfcTag(const NfcTag &rhs) { + uid_ = rhs.uid_; + tag_type_ = rhs.tag_type_; + if (rhs.ndef_message_ != nullptr) + ndef_message_ = make_unique(*rhs.ndef_message_); + } std::vector &get_uid() { return this->uid_; }; const std::string &get_tag_type() { return this->tag_type_; }; bool has_ndef_message() { return this->ndef_message_ != nullptr; }; - NdefMessage *get_ndef_message() { return this->ndef_message_; }; - void set_ndef_message(NdefMessage *ndef_message) { this->ndef_message_ = ndef_message; }; + const std::shared_ptr &get_ndef_message() { return this->ndef_message_; }; + void set_ndef_message(std::unique_ptr ndef_message) { this->ndef_message_ = std::move(ndef_message); }; protected: std::vector uid_; std::string tag_type_; - NdefMessage *ndef_message_{nullptr}; + std::shared_ptr ndef_message_; }; } // namespace nfc diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 80a11384b9..333dbc5a75 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -14,7 +14,7 @@ void NTC::setup() { void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this) } float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { - if (isnan(value)) { + if (std::isnan(value)) { this->publish_state(NAN); return; } diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index e7b8c03586..660208635c 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_VALUE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -106,9 +105,7 @@ def process_calibration(value): a, b, c = calc_steinhart_hart(value) else: raise cv.Invalid( - "Calibration parameter accepts either a list for steinhart-hart " - "calibration, or mapping for b-constant calibration, " - "not {}".format(type(value)) + f"Calibration parameter accepts either a list for steinhart-hart calibration, or mapping for b-constant calibration, not {type(value)}" ) return { @@ -120,7 +117,10 @@ def process_calibration(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py new file mode 100644 index 0000000000..2856a25ee7 --- /dev/null +++ b/esphome/components/number/__init__.py @@ -0,0 +1,162 @@ +from typing import Optional +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_ID, + CONF_ON_VALUE, + CONF_ON_VALUE_RANGE, + CONF_TRIGGER_ID, + CONF_MQTT_ID, + CONF_VALUE, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +number_ns = cg.esphome_ns.namespace("number") +Number = number_ns.class_("Number", cg.EntityBase) +NumberPtr = Number.operator("ptr") + +# Triggers +NumberStateTrigger = number_ns.class_( + "NumberStateTrigger", automation.Trigger.template(cg.float_) +) +ValueRangeTrigger = number_ns.class_( + "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component +) + +# Actions +NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) + +# Conditions +NumberInRangeCondition = number_ns.class_( + "NumberInRangeCondition", automation.Condition +) + +icon = cv.icon + + +NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), + cv.GenerateID(): cv.declare_id(Number), + 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.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + } +) + + +async def setup_number_core_( + var, config, *, min_value: float, max_value: float, step: Optional[float] +): + await setup_entity(var, config) + + cg.add(var.traits.set_min_value(min_value)) + cg.add(var.traits.set_max_value(max_value)) + if step is not None: + cg.add(var.traits.set_step(step)) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + 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) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_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) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_number( + var, config, *, min_value: float, max_value: float, step: Optional[float] = None +): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_number(var)) + await setup_number_core_( + var, config, min_value=min_value, max_value=max_value, step=step + ) + + +async def new_number( + config, *, min_value: float, max_value: float, step: Optional[float] = None +): + var = cg.new_Pvariable(config[CONF_ID]) + await register_number( + var, config, min_value=min_value, max_value=max_value, step=step + ) + return var + + +NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), +) + + +@automation.register_condition( + "number.in_range", NumberInRangeCondition, NUMBER_IN_RANGE_CONDITION_SCHEMA +) +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])) + + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_NUMBER") + cg.add_global(number_ns.using) + + +@automation.register_action( + "number.set", + NumberSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Required(CONF_VALUE): cv.templatable(cv.float_), + } + ), +) +async def number_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, float) + cg.add(var.set_value(template_)) + return var diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp new file mode 100644 index 0000000000..c75d272660 --- /dev/null +++ b/esphome/components/number/automation.cpp @@ -0,0 +1,47 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number.automation"; + +void ValueRangeTrigger::setup() { + this->rtc_ = global_preferences->make_preference(this->parent_->get_object_id_hash()); + bool initial_state; + if (this->rtc_.load(&initial_state)) { + this->previous_in_range_ = initial_state; + } + + this->parent_->add_on_state_callback([this](float state) { this->on_state_(state); }); +} +float ValueRangeTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } + +void ValueRangeTrigger::on_state_(float state) { + if (std::isnan(state)) + return; + + float local_min = this->min_.value(state); + float local_max = this->max_.value(state); + + bool in_range; + if (std::isnan(local_min) && std::isnan(local_max)) { + in_range = this->previous_in_range_; + } else if (std::isnan(local_min)) { + in_range = state <= local_max; + } else if (std::isnan(local_max)) { + in_range = state >= local_min; + } else { + in_range = local_min <= state && state <= local_max; + } + + if (in_range != this->previous_in_range_ && in_range) { + this->trigger(state); + } + + this->previous_in_range_ = in_range; + this->rtc_.save(&in_range); +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h new file mode 100644 index 0000000000..98554a346a --- /dev/null +++ b/esphome/components/number/automation.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace number { + +class NumberStateTrigger : public Trigger { + public: + explicit NumberStateTrigger(Number *parent) { + parent->add_on_state_callback([this](float value) { this->trigger(value); }); + } +}; + +template class NumberSetAction : public Action { + public: + NumberSetAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.set_value(this->value_.value(x...)); + call.perform(); + } + + protected: + Number *number_; +}; + +class ValueRangeTrigger : public Trigger, public Component { + public: + explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} + + template void set_min(V min) { this->min_ = min; } + template void set_max(V max) { this->max_ = max; } + + void setup() override; + float get_setup_priority() const override; + + protected: + void on_state_(float state); + + Number *parent_; + ESPPreferenceObject rtc_; + bool previous_in_range_{false}; + TemplatableValue min_{NAN}; + TemplatableValue max_{NAN}; +}; + +template class NumberInRangeCondition : public Condition { + public: + NumberInRangeCondition(Number *parent) : parent_(parent) {} + + void set_min(float min) { this->min_ = min; } + void set_max(float max) { this->max_ = max; } + bool check(Ts... x) override { + const float state = this->parent_->state; + if (std::isnan(this->min_)) { + return state <= this->max_; + } else if (std::isnan(this->max_)) { + return state >= this->min_; + } else { + return this->min_ <= state && state <= this->max_; + } + } + + protected: + Number *parent_; + float min_{NAN}; + float max_{NAN}; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp new file mode 100644 index 0000000000..57a5c7c4bd --- /dev/null +++ b/esphome/components/number/number.cpp @@ -0,0 +1,47 @@ +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (!this->value_.has_value() || std::isnan(*this->value_)) { + ESP_LOGW(TAG, "No value set for NumberCall"); + return; + } + + const auto &traits = this->parent_->traits; + auto value = *this->value_; + + float min_value = traits.get_min_value(); + if (value < min_value) { + ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value); + return; + } + float max_value = traits.get_max_value(); + if (value > max_value) { + ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value); + return; + } + ESP_LOGD(TAG, " Value: %f", *this->value_); + this->parent_->control(*this->value_); +} + +void Number::publish_state(float state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); + this->state_callback_.call(state); +} + +void Number::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +uint32_t Number::hash_base() { return 2282307003UL; } + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h new file mode 100644 index 0000000000..ed104fb477 --- /dev/null +++ b/esphome/components/number/number.h @@ -0,0 +1,89 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +#define LOG_NUMBER(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 Number; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + void perform(); + + NumberCall &set_value(float value) { + value_ = value; + return *this; + } + const optional &get_value() const { return value_; } + + protected: + Number *const parent_; + optional value_; +}; + +class NumberTraits { + public: + void set_min_value(float min_value) { min_value_ = min_value; } + float get_min_value() const { return min_value_; } + void set_max_value(float max_value) { max_value_ = max_value; } + float get_max_value() const { return max_value_; } + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; +}; + +/** Base-class for all numbers. + * + * A number can use publish_state to send out a new value. + */ +class Number : public EntityBase { + public: + float state; + + void publish_state(float state); + + NumberCall make_call() { return NumberCall(this); } + void set(float value) { make_call().set_value(value).perform(); } + + void add_on_state_callback(std::function &&callback); + + NumberTraits traits; + + /// Return whether this number has gotten a full state yet. + bool has_state() const { return has_state_; } + + protected: + friend class NumberCall; + + /** Set the value of the number, this is a virtual method that each number integration must implement. + * + * This method is called by the NumberCall. + * + * @param value The value as validated by the NumberCall. + */ + virtual void control(float value) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_; + bool has_state_{false}; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 7ee7ef47ca..bcfb28979d 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,6 +1,7 @@ from esphome.cpp_generator import RawExpression import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.const import ( CONF_ID, CONF_NUM_ATTEMPTS, @@ -8,25 +9,75 @@ from esphome.const import ( CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, + CONF_TRIGGER_ID, ) from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +AUTO_LOAD = ["socket"] + +CONF_ON_STATE_CHANGE = "on_state_change" +CONF_ON_BEGIN = "on_begin" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_END = "on_end" +CONF_ON_ERROR = "on_error" ota_ns = cg.esphome_ns.namespace("ota") +OTAState = ota_ns.enum("OTAState") OTAComponent = ota_ns.class_("OTAComponent", cg.Component) +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()) + + +def validate_password_support(value): + if CORE.using_arduino: + return value + if CORE.using_esp_idf: + raise cv.Invalid("Password support is not implemented yet for ESP-IDF") + raise NotImplementedError + CONFIG_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): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string, + cv.Optional(CONF_PASSWORD): cv.All(cv.string, validate_password_support), 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_BEGIN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger), + } + ), + cv.Optional(CONF_ON_PROGRESS): automation.validate_automation( + { + 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) @@ -35,7 +86,9 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_port(config[CONF_PORT])) - cg.add(var.set_auth_password(config[CONF_PASSWORD])) + 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) @@ -47,5 +100,29 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("Update", None) - elif CORE.is_esp32: - cg.add_library("Hash", None) + elif CORE.is_esp32 and CORE.using_arduino: + cg.add_library("Update", None) + + 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_BEGIN, []): + 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_PROGRESS, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + use_state_callback = True + for conf in config.get(CONF_ON_END, []): + 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_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(int, "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 new file mode 100644 index 0000000000..6c8aca3705 --- /dev/null +++ b/esphome/components/ota/automation.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_OTA_STATE_CALLBACK + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/ota/ota_component.h" + +namespace esphome { +namespace ota { + +class OTAStateChangeTrigger : public Trigger { + public: + explicit OTAStateChangeTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (!parent->is_failed()) { + return trigger(state); + } + }); + } +}; + +class OTAStartTrigger : public Trigger<> { + public: + explicit OTAStartTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_STARTED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class OTAProgressTrigger : public Trigger { + public: + explicit OTAProgressTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_IN_PROGRESS && !parent->is_failed()) { + trigger(progress); + } + }); + } +}; + +class OTAEndTrigger : public Trigger<> { + public: + explicit OTAEndTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_COMPLETED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class OTAErrorTrigger : public Trigger { + public: + explicit OTAErrorTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_ERROR && !parent->is_failed()) { + trigger(error); + } + }); + } +}; + +} // namespace ota +} // namespace esphome + +#endif // USE_OTA_STATE_CALLBACK diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h new file mode 100644 index 0000000000..c253e009c6 --- /dev/null +++ b/esphome/components/ota/ota_backend.h @@ -0,0 +1,18 @@ +#pragma once +#include "ota_component.h" + +namespace esphome { +namespace ota { + +class OTABackend { + public: + virtual ~OTABackend() = default; + virtual OTAResponseTypes begin(size_t image_size) = 0; + virtual void set_update_md5(const char *md5) = 0; + virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0; + virtual OTAResponseTypes end() = 0; + virtual void abort() = 0; +}; + +} // namespace ota +} // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp new file mode 100644 index 0000000000..4759737dbd --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp @@ -0,0 +1,46 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "ota_backend_arduino_esp32.h" +#include "ota_component.h" +#include "ota_backend.h" + +#include + +namespace esphome { +namespace ota { + +OTAResponseTypes ArduinoESP32OTABackend::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; + return OTA_RESPONSE_ERROR_UNKNOWN; +} + +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; + } + return OTA_RESPONSE_OK; +} + +OTAResponseTypes ArduinoESP32OTABackend::end() { + if (!Update.end()) + return OTA_RESPONSE_ERROR_UPDATE_END; + return OTA_RESPONSE_OK; +} + +void ArduinoESP32OTABackend::abort() { Update.abort(); } + +} // namespace ota +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h new file mode 100644 index 0000000000..8343bdf94f --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -0,0 +1,22 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "ota_component.h" +#include "ota_backend.h" + +namespace esphome { +namespace ota { + +class ArduinoESP32OTABackend : public OTABackend { + 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; +}; + +} // namespace ota +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp new file mode 100644 index 0000000000..23dc0d4e21 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp @@ -0,0 +1,59 @@ +#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 + +namespace esphome { +namespace ota { + +OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { + bool ret = Update.begin(image_size, U_FLASH); + if (ret) { + esp8266::preferences_prevent_write(true); + return OTA_RESPONSE_OK; + } + + uint8_t error = Update.getError(); + if (error == UPDATE_ERROR_BOOTSTRAP) + return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; + if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) + return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; + if (error == UPDATE_ERROR_FLASH_CONFIG) + return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; + if (error == UPDATE_ERROR_SPACE) + return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; + return OTA_RESPONSE_ERROR_UNKNOWN; +} + +void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } + +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; + } + return OTA_RESPONSE_OK; +} + +OTAResponseTypes ArduinoESP8266OTABackend::end() { + if (!Update.end()) + return OTA_RESPONSE_ERROR_UPDATE_END; + return OTA_RESPONSE_OK; +} + +void ArduinoESP8266OTABackend::abort() { + Update.end(); + esp8266::preferences_prevent_write(false); +} + +} // namespace ota +} // namespace esphome + +#endif +#endif diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h new file mode 100644 index 0000000000..d1195af911 --- /dev/null +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -0,0 +1,25 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 + +#include "ota_component.h" +#include "ota_backend.h" + +namespace esphome { +namespace ota { + +class ArduinoESP8266OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; +}; + +} // namespace ota +} // namespace esphome + +#endif +#endif diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp new file mode 100644 index 0000000000..4eb17d82f1 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -0,0 +1,72 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF + +#include "ota_backend_esp_idf.h" +#include "ota_component.h" +#include + +namespace esphome { +namespace ota { + +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; + } + esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + if (err == ESP_ERR_INVALID_SIZE) { + return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; + } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + return OTA_RESPONSE_ERROR_UNKNOWN; + } + return OTA_RESPONSE_OK; +} + +void IDFOTABackend::set_update_md5(const char *md5) { + // pass +} + +OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { + esp_err_t err = esp_ota_write(this->update_handle_, data, len); + if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + return OTA_RESPONSE_ERROR_MAGIC; + } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + return OTA_RESPONSE_ERROR_UNKNOWN; + } + return OTA_RESPONSE_OK; +} + +OTAResponseTypes IDFOTABackend::end() { + esp_err_t err = esp_ota_end(this->update_handle_); + this->update_handle_ = 0; + if (err == ESP_OK) { + err = esp_ota_set_boot_partition(this->partition_); + if (err == ESP_OK) { + return OTA_RESPONSE_OK; + } + } + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + return OTA_RESPONSE_ERROR_UPDATE_END; + } + if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + return OTA_RESPONSE_ERROR_UNKNOWN; +} + +void IDFOTABackend::abort() { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; +} + +} // namespace ota +} // namespace esphome +#endif diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h new file mode 100644 index 0000000000..d6e2e2742a --- /dev/null +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -0,0 +1,27 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF + +#include "ota_component.h" +#include "ota_backend.h" +#include + +namespace esphome { +namespace ota { + +class IDFOTABackend : 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; + + private: + esp_ota_handle_t update_handle_{0}; + const esp_partition_t *partition_; +}; + +} // namespace ota +} // namespace esphome +#endif diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 5302b7bc24..9ad3814f5c 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -1,16 +1,21 @@ #include "ota_component.h" +#include "ota_backend.h" +#include "ota_backend_arduino_esp32.h" +#include "ota_backend_arduino_esp8266.h" +#include "ota_backend_esp_idf.h" #include "esphome/core/log.h" -#include "esphome/core/helpers.h" #include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/util.h" +#include "esphome/components/network/util.h" +#include #include + +#ifdef USE_OTA_PASSWORD #include -#ifdef ARDUINO_ARCH_ESP32 -#include #endif -#include namespace esphome { namespace ota { @@ -19,19 +24,73 @@ static const char *const TAG = "ota"; static const uint8_t OTA_VERSION_1_0 = 1; +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 +} + void OTAComponent::setup() { - this->server_ = new WiFiServer(this->port_); - this->server_->begin(); + server_ = socket::socket(AF_INET, 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_in server; + memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); + + 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_address().c_str(), this->port_); + 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."); } - if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1) { +#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_); } @@ -51,28 +110,37 @@ void OTAComponent::loop() { void OTAComponent::handle_() { OTAResponseTypes error_code = OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; - uint32_t total = 0; + size_t total = 0; uint32_t last_progress = 0; uint8_t buf[1024]; char *sbuf = reinterpret_cast(buf); - uint32_t ota_size; + size_t ota_size; uint8_t ota_features; + std::unique_ptr backend; (void) ota_features; - if (!this->client_.connected()) { - this->client_ = this->server_->available(); + 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; - if (!this->client_.connected()) - 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; } - // enable nodelay for outgoing data - this->client_.setNoDelay(true); - - ESP_LOGD(TAG, "Starting OTA Update from %s...", this->client_.remoteIP().toString().c_str()); + 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->wait_receive_(buf, 5)) { + if (!this->readall_(buf, 5)) { ESP_LOGW(TAG, "Reading magic bytes failed!"); goto error; } @@ -85,11 +153,12 @@ void OTAComponent::handle_() { } // Send OK and version - 2 bytes - this->client_.write(OTA_RESPONSE_OK); - this->client_.write(OTA_VERSION_1_0); + buf[0] = OTA_RESPONSE_OK; + buf[1] = OTA_VERSION_1_0; + this->writeall_(buf, 2); // Read features - 1 byte - if (!this->wait_receive_(buf, 1)) { + if (!this->readall_(buf, 1)) { ESP_LOGW(TAG, "Reading features failed!"); goto error; } @@ -97,10 +166,13 @@ void OTAComponent::handle_() { ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features); // Acknowledge header - 1 byte - this->client_.write(OTA_RESPONSE_HEADER_OK); + buf[0] = OTA_RESPONSE_HEADER_OK; + this->writeall_(buf, 1); +#ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { - this->client_.write(OTA_RESPONSE_REQUEST_AUTH); + buf[0] = OTA_RESPONSE_REQUEST_AUTH; + this->writeall_(buf, 1); MD5Builder md5_builder{}; md5_builder.begin(); sprintf(sbuf, "%08X", random_uint32()); @@ -110,7 +182,7 @@ void OTAComponent::handle_() { ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); // Send nonce, 32 bytes hex MD5 - if (this->client_.write(reinterpret_cast(sbuf), 32) != 32) { + if (!this->writeall_(reinterpret_cast(sbuf), 32)) { ESP_LOGW(TAG, "Auth: Writing nonce failed!"); goto error; } @@ -122,7 +194,7 @@ void OTAComponent::handle_() { md5_builder.add(sbuf); // Receive cnonce, 32 bytes hex MD5 - if (!this->wait_receive_(buf, 32)) { + if (!this->readall_(buf, 32)) { ESP_LOGW(TAG, "Auth: Reading cnonce failed!"); goto error; } @@ -137,7 +209,7 @@ void OTAComponent::handle_() { ESP_LOGV(TAG, "Auth: Result is %s", sbuf); // Receive result, 32 bytes hex MD5 - if (!this->wait_receive_(buf + 64, 32)) { + if (!this->readall_(buf + 64, 32)) { ESP_LOGW(TAG, "Auth: Reading response failed!"); goto error; } @@ -154,12 +226,14 @@ void OTAComponent::handle_() { goto error; } } +#endif // USE_OTA_PASSWORD // Acknowledge auth OK - 1 byte - this->client_.write(OTA_RESPONSE_AUTH_OK); + buf[0] = OTA_RESPONSE_AUTH_OK; + this->writeall_(buf, 1); // Read size, 4 bytes MSB first - if (!this->wait_receive_(buf, 4)) { + if (!this->readall_(buf, 4)) { ESP_LOGW(TAG, "Reading size failed!"); goto error; } @@ -170,205 +244,205 @@ void OTAComponent::handle_() { } ESP_LOGV(TAG, "OTA size is %u bytes", ota_size); -#ifdef ARDUINO_ARCH_ESP8266 - global_preferences.prevent_write(true); -#endif - - if (!Update.begin(ota_size, U_FLASH)) { - StreamString ss; - Update.printError(ss); -#ifdef ARDUINO_ARCH_ESP8266 - if (ss.indexOf("Invalid bootstrapping") != -1) { - error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - goto error; - } - if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) { - error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - goto error; - } - if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) { - error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - goto error; - } - if (ss.indexOf("Not Enough Space") != -1) { - error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; - goto error; - } -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (ss.indexOf("Bad Size Given") != -1) { - error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - goto error; - } -#endif - ESP_LOGW(TAG, "Preparing OTA partition failed! '%s'", ss.c_str()); - error_code = OTA_RESPONSE_ERROR_UPDATE_PREPARE; + backend = make_ota_backend(); + error_code = backend->begin(ota_size); + if (error_code != OTA_RESPONSE_OK) goto error; - } update_started = true; // Acknowledge prepare OK - 1 byte - this->client_.write(OTA_RESPONSE_UPDATE_PREPARE_OK); + buf[0] = OTA_RESPONSE_UPDATE_PREPARE_OK; + this->writeall_(buf, 1); // Read binary MD5, 32 bytes - if (!this->wait_receive_(buf, 32)) { + if (!this->readall_(buf, 32)) { ESP_LOGW(TAG, "Reading binary MD5 checksum failed!"); goto error; } sbuf[32] = '\0'; ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); - Update.setMD5(sbuf); + backend->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte - this->client_.write(OTA_RESPONSE_BIN_MD5_OK); + buf[0] = OTA_RESPONSE_BIN_MD5_OK; + this->writeall_(buf, 1); - while (!Update.isFinished()) { - size_t available = this->wait_receive_(buf, 0); - if (!available) { + 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) { + delay(1); + continue; + } + ESP_LOGW(TAG, "Error receiving data for update, errno: %d", errno); goto error; } - uint32_t written = Update.write(buf, available); - if (written != available) { - ESP_LOGW(TAG, "Error writing binary data to flash: %u != %u!", written, available); // NOLINT - error_code = OTA_RESPONSE_ERROR_WRITING_FLASH; + error_code = backend->write(buf, read); + if (error_code != OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error writing binary data to flash!"); goto error; } - total += written; + 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 // slow down OTA update to avoid getting killed by task watchdog (task_wdt) delay(10); } } // Acknowledge receive OK - 1 byte - this->client_.write(OTA_RESPONSE_RECEIVE_OK); + buf[0] = OTA_RESPONSE_RECEIVE_OK; + this->writeall_(buf, 1); - if (!Update.end()) { - error_code = OTA_RESPONSE_ERROR_UPDATE_END; + error_code = backend->end(); + if (error_code != OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error ending OTA!"); goto error; } // Acknowledge Update end OK - 1 byte - this->client_.write(OTA_RESPONSE_UPDATE_END_OK); + buf[0] = OTA_RESPONSE_UPDATE_END_OK; + this->writeall_(buf, 1); // Read ACK - if (!this->wait_receive_(buf, 1, false) || buf[0] != OTA_RESPONSE_OK) { + 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_.flush(); - this->client_.stop(); + 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: - if (update_started) { - StreamString ss; - Update.printError(ss); - ESP_LOGW(TAG, "Update end failed! Error: %s", ss.c_str()); - } - if (this->client_.connected()) { - this->client_.write(static_cast(error_code)); - this->client_.flush(); - } - this->client_.stop(); + buf[0] = static_cast(error_code); + this->writeall_(buf, 1); + this->client_->close(); + this->client_ = nullptr; -#ifdef ARDUINO_ARCH_ESP32 - if (update_started) { - Update.abort(); + if (backend != nullptr && update_started) { + backend->abort(); } -#endif - -#ifdef ARDUINO_ARCH_ESP8266 - if (update_started) { - Update.end(); - } -#endif this->status_momentary_error("onerror", 5000); - -#ifdef ARDUINO_ARCH_ESP8266 - global_preferences.prevent_write(false); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_ERROR, 0.0f, static_cast(error_code)); #endif } -size_t OTAComponent::wait_receive_(uint8_t *buf, size_t bytes, bool check_disconnected) { - size_t available = 0; +bool OTAComponent::readall_(uint8_t *buf, size_t len) { uint32_t start = millis(); - do { - App.feed_wdt(); - if (check_disconnected && !this->client_.connected()) { - ESP_LOGW(TAG, "Error client disconnected while receiving data!"); - return 0; - } - int availi = this->client_.available(); - if (availi < 0) { - ESP_LOGW(TAG, "Error reading data!"); - return 0; - } + uint32_t at = 0; + while (len - at > 0) { uint32_t now = millis(); - if (availi == 0 && now - start > 10000) { - ESP_LOGW(TAG, "Timeout waiting for data!"); - return 0; + if (now - start > 1000) { + ESP_LOGW(TAG, "Timed out reading %d bytes of data", len); + return false; } - available = size_t(availi); - yield(); - } while (bytes == 0 ? available == 0 : available < bytes); - if (bytes == 0) - bytes = std::min(available, size_t(1024)); - - bool success = false; - for (uint32_t i = 0; !success && i < 100; i++) { - int res = this->client_.read(buf, bytes); - - if (res != int(bytes)) { - // ESP32 implementation has an issue where calling read can fail with EAGAIN (race condition) - // so just re-try it until it works (with generous timeout of 1s) - // because we check with available() first this should not cause us any trouble in all other cases - delay(10); + ssize_t read = this->client_->read(buf + at, len - at); + if (read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno); + return false; } else { - success = true; + at += read; } + delay(1); } - if (!success) { - ESP_LOGW(TAG, "Reading %u bytes of binary data failed!", bytes); // NOLINT - return 0; - } - - return bytes; + 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; + } -void OTAComponent::set_auth_password(const std::string &password) { this->password_ = password; } + ssize_t written = this->client_->write(buf + at, len - at); + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + delay(1); + continue; + } + ESP_LOGW(TAG, "Failed to write %d bytes of data, errno: %d", len, errno); + return false; + } else { + at += written; + } + 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->rtc_ = global_preferences->make_preference(233825507UL, false); this->safe_mode_rtc_value_ = this->read_rtc_(); - ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); + bool is_manual_safe_mode = this->safe_mode_rtc_value_ == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC; - if (this->safe_mode_rtc_value_ >= num_attempts) { + 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(); - ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode."); + if (!is_manual_safe_mode) + ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode."); this->status_set_error(); this->set_timeout(enable_time, []() { @@ -387,7 +461,10 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ return false; } } -void OTAComponent::write_rtc_(uint32_t val) { this->rtc_.save(&val); } +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)) @@ -396,9 +473,15 @@ uint32_t OTAComponent::read_rtc_() { } void OTAComponent::clean_rtc() { this->write_rtc_(0); } void OTAComponent::on_safe_shutdown() { - if (this->has_safe_mode_) + 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 index f16725e324..e08e187df6 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -1,9 +1,10 @@ #pragma once +#include "esphome/components/socket/socket.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include -#include +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" namespace esphome { namespace ota { @@ -29,26 +30,32 @@ enum OTAResponseTypes { 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_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: - /** Set a plaintext password that OTA will use for authentication. - * - * Warning: This password will be stored in plaintext in the ROM and can be read - * by intruders. - * - * @param password The plaintext password. - */ - void set_auth_password(const std::string &password); +#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; @@ -67,14 +74,17 @@ class OTAComponent : public Component { uint32_t read_rtc_(); void handle_(); - size_t wait_receive_(uint8_t *buf, size_t bytes, bool check_disconnected = true); + 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_; - WiFiServer *server_{nullptr}; - WiFiClient client_{}; + 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. @@ -82,6 +92,13 @@ class OTAComponent : public Component { 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 }; } // namespace ota diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4471794033..4f1fb33fe7 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -17,6 +17,8 @@ from esphome.core import CORE CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +CONF_ZERO_MEANS_ZERO = "zero_means_zero" + BINARY_OUTPUT_SCHEMA = cv.Schema( { cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), @@ -28,6 +30,7 @@ FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend( { cv.Optional(CONF_MAX_POWER): cv.percentage, cv.Optional(CONF_MIN_POWER): cv.percentage, + cv.Optional(CONF_ZERO_MEANS_ZERO, default=False): cv.boolean, } ) @@ -53,6 +56,8 @@ async def setup_output_platform_(obj, config): cg.add(obj.set_max_power(config[CONF_MAX_POWER])) if CONF_MIN_POWER in config: cg.add(obj.set_min_power(config[CONF_MIN_POWER])) + if CONF_ZERO_MEANS_ZERO in config: + cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO])) async def register_output(var, config): diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index f44383db36..f120f86f1f 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -17,6 +17,8 @@ void FloatOutput::set_min_power(float min_power) { this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX } +void FloatOutput::set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } + float FloatOutput::get_min_power() const { return this->min_power_; } void FloatOutput::set_level(float state) { @@ -29,10 +31,13 @@ void FloatOutput::set_level(float state) { this->power_.unrequest(); } #endif + + if (!(state == 0.0f && this->zero_means_zero_)) // regardless of min_power_, 0.0 means off + state = (state * (this->max_power_ - this->min_power_)) + this->min_power_; + if (this->is_inverted()) state = 1.0f - state; - float adjusted_value = (state * (this->max_power_ - this->min_power_)) + this->min_power_; - this->write_state(adjusted_value); + this->write_state(state); } void FloatOutput::write_state(bool state) { this->set_level(state != this->inverted_ ? 1.0f : 0.0f); } diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 1b969c9225..3e2b3ada8d 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,6 +46,12 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); + /** Sets this output to ignore min_power for a 0 state + * + * @param zero True if a 0 state should mean 0 and not min_power. + */ + void set_zero_means_zero(bool zero_means_zero); + /** Set the level of this float output, this is called from the front-end. * * @param state The new state. @@ -76,6 +82,7 @@ class FloatOutput : public BinaryOutput { float max_power_{1.0f}; float min_power_{0.0f}; + bool zero_means_zero_; }; } // namespace output diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 8c5c9a0144..df0f0de13d 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,6 +1,19 @@ +import re +from pathlib import Path +from esphome.core import EsphomeError + +from esphome import git, yaml_util +from esphome.const import ( + CONF_FILE, + CONF_FILES, + CONF_PACKAGES, + CONF_REF, + CONF_REFRESH, + CONF_URL, +) import esphome.config_validation as cv -from esphome.const import CONF_PACKAGES +DOMAIN = CONF_PACKAGES def _merge_package(full_old, full_new): @@ -23,20 +36,129 @@ def _merge_package(full_old, full_new): return merge(full_old, full_new) +def validate_git_package(config: dict): + new_config = config + for key, conf in config.items(): + if CONF_URL in conf: + try: + conf = BASE_SCHEMA(conf) + if CONF_FILE in conf: + new_config[key][CONF_FILES] = [conf[CONF_FILE]] + del new_config[key][CONF_FILE] + except cv.MultipleInvalid as e: + with cv.prepend_path([key]): + raise e + except cv.Invalid as e: + raise cv.Invalid( + "Extra keys not allowed in git based package", + path=[key] + e.path, + ) from e + return new_config + + +def validate_yaml_filename(value): + value = cv.string(value) + + if not (value.endswith(".yaml") or value.endswith(".yml")): + raise cv.Invalid("Only YAML (.yaml / .yml) files are supported.") + + return value + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + + m = re.match( + r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)/([a-zA-Z0-9\-_.\./]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path or in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!" + ) + + conf = { + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + CONF_FILE: m.group(3), + } + if m.group(4): + conf[CONF_REF] = m.group(4) + + # print(conf) + return BASE_SCHEMA(conf) + + +BASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_URL): cv.url, + cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, + cv.Exclusive(CONF_FILES, "files"): cv.All( + cv.ensure_list(validate_yaml_filename), + cv.Length(min=1), + ), + cv.Optional(CONF_REF): cv.git_ref, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ), + cv.has_at_least_one_key(CONF_FILE, CONF_FILES), +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), + } + ), + validate_git_package, +) + + +def _process_base_package(config: dict) -> dict: + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=config[CONF_REFRESH], + domain=DOMAIN, + ) + files: str = config[CONF_FILES] + + packages = {} + for file in files: + yaml_file: Path = repo_dir / file + + if not yaml_file.is_file(): + raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES]) + + try: + packages[file] = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{file} is not a valid YAML file. Please check the file contents." + ) from e + return {"packages": packages} + + def do_packages_pass(config: dict): if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] with cv.prepend_path(CONF_PACKAGES): + packages = CONFIG_SCHEMA(packages) if not isinstance(packages, dict): raise cv.Invalid( - "Packages must be a key to value mapping, got {} instead" - "".format(type(packages)) + f"Packages must be a key to value mapping, got {type(packages)} instead" ) for package_name, package_config in packages.items(): with cv.prepend_path(package_name): recursive_package = package_config + if CONF_URL in package_config: + package_config = _process_base_package(package_config) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config) config = _merge_package(recursive_package, config) diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index 5ded6b906c..822b7ac306 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -1,10 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome.components import light from esphome.const import ( + CONF_ADDRESSABLE_LIGHT_ID, CONF_FROM, CONF_ID, + CONF_LIGHT_ID, + CONF_NUM_LEDS, CONF_SEGMENTS, + CONF_SINGLE_LIGHT_ID, CONF_TO, CONF_OUTPUT_ID, CONF_REVERSED, @@ -12,31 +17,68 @@ from esphome.const import ( partitions_ns = cg.esphome_ns.namespace("partition") AddressableSegment = partitions_ns.class_("AddressableSegment") +AddressableLightWrapper = cg.esphome_ns.namespace("light").class_( + "AddressableLightWrapper" +) PartitionLightOutput = partitions_ns.class_( "PartitionLightOutput", light.AddressableLight ) def validate_from_to(value): - if value[CONF_FROM] > value[CONF_TO]: + if CONF_ID in value and value[CONF_FROM] > value[CONF_TO]: raise cv.Invalid( - "From ({}) must not be larger than to ({})" - "".format(value[CONF_FROM], value[CONF_TO]) + f"From ({value[CONF_FROM]}) must not be larger than to ({value[CONF_TO]})" ) return value +def validate_segment(config): + fconf = fv.full_config.get() + + if CONF_ID in config: # only validate addressable segments + path = fconf.get_path_for_id(config[CONF_ID])[:-1] + segment_light_config = fconf.get_config_for_path(path) + + if CONF_NUM_LEDS in segment_light_config: + segment_len = segment_light_config[CONF_NUM_LEDS] + if config[CONF_FROM] >= segment_len: + raise cv.Invalid( + f"FROM ({config[CONF_FROM]}) must be less than the number of LEDs in light '{config[CONF_ID]}' ({segment_len})", + [CONF_FROM], + ) + if config[CONF_TO] >= segment_len: + raise cv.Invalid( + f"TO ({config[CONF_TO]}) must be less than the number of LEDs in light '{config[CONF_ID]}' ({segment_len})", + [CONF_TO], + ) + + +ADDRESSABLE_SEGMENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(light.AddressableLightState), + cv.Required(CONF_FROM): cv.positive_int, + cv.Required(CONF_TO): cv.positive_int, + cv.Optional(CONF_REVERSED, default=False): cv.boolean, + } +) + +NONADDRESSABLE_SEGMENT_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_SINGLE_LIGHT_ID): cv.use_id(light.LightState), + cv.GenerateID(CONF_ADDRESSABLE_LIGHT_ID): cv.declare_id( + AddressableLightWrapper + ), + cv.GenerateID(CONF_LIGHT_ID): cv.declare_id(light.types.LightState), + } +) + CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(PartitionLightOutput), cv.Required(CONF_SEGMENTS): cv.All( cv.ensure_list( - { - cv.Required(CONF_ID): cv.use_id(light.AddressableLightState), - cv.Required(CONF_FROM): cv.positive_int, - cv.Required(CONF_TO): cv.positive_int, - cv.Optional(CONF_REVERSED, default=False): cv.boolean, - }, + cv.Any(ADDRESSABLE_SEGMENT_SCHEMA, NONADDRESSABLE_SEGMENT_SCHEMA), validate_from_to, ), cv.Length(min=1), @@ -44,19 +86,36 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( } ) +FINAL_VALIDATE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SEGMENTS): [validate_segment], + }, + extra=cv.ALLOW_EXTRA, +) + async def to_code(config): segments = [] for conf in config[CONF_SEGMENTS]: - var = await cg.get_variable(conf[CONF_ID]) - segments.append( - AddressableSegment( - var, - conf[CONF_FROM], - conf[CONF_TO] - conf[CONF_FROM] + 1, - conf[CONF_REVERSED], + if CONF_SINGLE_LIGHT_ID in conf: + wrapper = cg.new_Pvariable( + conf[CONF_ADDRESSABLE_LIGHT_ID], + await cg.get_variable(conf[CONF_SINGLE_LIGHT_ID]), + ) + light_state = cg.new_Pvariable(conf[CONF_LIGHT_ID], "", wrapper) + await cg.register_component(light_state, conf) + cg.add(cg.App.register_light(light_state)) + segments.append(AddressableSegment(light_state, 0, 1, False)) + + else: + segments.append( + AddressableSegment( + await cg.get_variable(conf[CONF_ID]), + conf[CONF_FROM], + conf[CONF_TO] - conf[CONF_FROM] + 1, + conf[CONF_REVERSED], + ) ) - ) var = cg.new_Pvariable(config[CONF_OUTPUT_ID], segments) await cg.register_component(var, config) diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index 687fe562d1..f74001cf75 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -50,13 +50,11 @@ class PartitionLightOutput : public light::AddressableLight { } } light::LightTraits get_traits() override { return this->segments_[0].get_src()->get_traits(); } - void loop() override { - if (this->should_show_()) { - for (auto seg : this->segments_) { - seg.get_src()->schedule_show(); - } - this->mark_shown_(); + void write_state(light::LightState *state) override { + for (auto seg : this->segments_) { + seg.get_src()->schedule_show(); } + this->mark_shown_(); } protected: diff --git a/esphome/components/pca9685/output.py b/esphome/components/pca9685/output.py index 40f7b3cd74..b7681f9ba0 100644 --- a/esphome/components/pca9685/output.py +++ b/esphome/components/pca9685/output.py @@ -20,6 +20,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): paren = await cg.get_variable(config[CONF_PCA9685_ID]) - rhs = paren.create_channel(config[CONF_CHANNEL]) - var = cg.Pvariable(config[CONF_ID], rhs) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(paren.register_channel(var)) await output.register_output(var, config) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index c3fd2b60e4..1ad6f4a665 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -123,11 +123,11 @@ void PCA9685Output::loop() { this->update_ = false; } -PCA9685Channel *PCA9685Output::create_channel(uint8_t channel) { - this->min_channel_ = std::min(this->min_channel_, channel); - this->max_channel_ = std::max(this->max_channel_, channel); - auto *c = new PCA9685Channel(this, channel); - return c; +void PCA9685Output::register_channel(PCA9685Channel *channel) { + auto c = channel->channel_; + this->min_channel_ = std::min(this->min_channel_, c); + this->max_channel_ = std::max(this->max_channel_, c); + channel->set_parent(this); } void PCA9685Channel::write_state(float state) { diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 55b52fc660..5dd52b5510 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -22,13 +22,16 @@ class PCA9685Output; class PCA9685Channel : public output::FloatOutput { public: - PCA9685Channel(PCA9685Output *parent, uint8_t channel) : parent_(parent), channel_(channel) {} + void set_channel(uint8_t channel) { channel_ = channel; } + void set_parent(PCA9685Output *parent) { parent_ = parent; } protected: + friend class PCA9685Output; + void write_state(float state) override; - PCA9685Output *parent_; uint8_t channel_; + PCA9685Output *parent_; }; /// PCA9685 float output component. @@ -37,7 +40,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice { PCA9685Output(float frequency, uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE) : frequency_(frequency), mode_(mode) {} - PCA9685Channel *create_channel(uint8_t channel); + void register_channel(PCA9685Channel *channel); void setup() override; void dump_config() override; diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index e96c526cb0..a5f963707f 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -2,17 +2,19 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import i2c -from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, +) DEPENDENCIES = ["i2c"] MULTI_CONF = True pcf8574_ns = cg.esphome_ns.namespace("pcf8574") -PCF8574GPIOMode = pcf8574_ns.enum("PCF8574GPIOMode") -PCF8674_GPIO_MODES = { - "INPUT": PCF8574GPIOMode.PCF8574_INPUT, - "OUTPUT": PCF8574GPIOMode.PCF8574_OUTPUT, -} PCF8574Component = pcf8574_ns.class_("PCF8574Component", cg.Component, i2c.I2CDevice) PCF8574GPIOPin = pcf8574_ns.class_("PCF8574GPIOPin", cg.GPIOPin) @@ -38,39 +40,40 @@ async def to_code(config): cg.add(var.set_pcf8575(config[CONF_PCF8575])) -def validate_pcf8574_gpio_mode(value): - value = cv.string(value) - if value.upper() == "INPUT_PULLUP": - raise cv.Invalid( - "INPUT_PULLUP mode has been removed in 1.14 and been combined into " - "INPUT mode (they were the same thing). Please use INPUT instead." - ) - return cv.enum(PCF8674_GPIO_MODES, upper=True)(value) +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 -PCF8574_OUTPUT_PIN_SCHEMA = cv.Schema( +PCF8574_PIN_SCHEMA = cv.All( { + cv.GenerateID(): cv.declare_id(PCF8574GPIOPin), cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): validate_pcf8574_gpio_mode, - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) -PCF8574_INPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): validate_pcf8574_gpio_mode, + 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_OUTPUT_PIN_SCHEMA, PCF8574_INPUT_PIN_SCHEMA) -) +@pins.PIN_SCHEMA_REGISTRY.register("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]) - return PCF8574GPIOPin.new( - parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED] - ) + + 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/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 02817df888..6eaf73e8da 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -38,20 +38,15 @@ void PCF8574Component::digital_write(uint8_t pin, bool value) { this->write_gpio_(); } -void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) { - switch (mode) { - case PCF8574_INPUT: - // Clear mode mask bit - this->mode_mask_ &= ~(1 << pin); - // Write GPIO to enable input mode - this->write_gpio_(); - break; - case PCF8574_OUTPUT: - // Set mode mask bit - this->mode_mask_ |= 1 << pin; - break; - default: - break; +void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + // Write GPIO to enable input mode + this->write_gpio_(); + } else if (flags == gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; } } bool PCF8574Component::read_gpio_() { @@ -87,7 +82,7 @@ bool PCF8574Component::write_gpio_() { uint8_t data[2]; data[0] = value; data[1] = value >> 8; - if (!this->write_bytes_raw(data, this->pcf8575_ ? 2 : 1)) { + if (this->write(data, this->pcf8575_ ? 2 : 1) != i2c::ERROR_OK) { this->status_set_warning(); return false; } @@ -97,12 +92,15 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } -void PCF8574GPIOPin::setup() { this->pin_mode(this->mode_); } +void PCF8574GPIOPin::setup() { pin_mode(flags_); } +void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -void PCF8574GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } -PCF8574GPIOPin::PCF8574GPIOPin(PCF8574Component *parent, uint8_t pin, uint8_t mode, bool inverted) - : GPIOPin(pin, mode, inverted), parent_(parent) {} +std::string PCF8574GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via PCF8574", pin_); + return buffer; +} } // namespace pcf8574 } // namespace esphome diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 925fa30899..c201e0615f 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -1,18 +1,12 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" namespace esphome { namespace pcf8574 { -/// Modes for PCF8574 pins -enum PCF8574GPIOMode : uint8_t { - PCF8574_INPUT = INPUT, - PCF8574_OUTPUT = OUTPUT, -}; - class PCF8574Component : public Component, public i2c::I2CDevice { public: PCF8574Component() = default; @@ -26,7 +20,7 @@ class PCF8574Component : public Component, public i2c::I2CDevice { /// 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, uint8_t mode); + void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; @@ -49,15 +43,22 @@ class PCF8574Component : public Component, public i2c::I2CDevice { /// Helper class to expose a PCF8574 pin as an internal input GPIO pin. class PCF8574GPIOPin : public GPIOPin { public: - PCF8574GPIOPin(PCF8574Component *parent, uint8_t pin, uint8_t mode, bool inverted = false); - void setup() override; - void pin_mode(uint8_t mode) 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(PCF8574Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } protected: PCF8574Component *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; }; } // namespace pcf8574 diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 83e4b6be32..15c1c5f076 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -72,7 +72,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce return res; } - if (!isnan(this->setpoint_) && this->setpoint_ != setpoint) { + if (!std::isnan(this->setpoint_) && this->setpoint_ != setpoint) { ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!"); } this->setpoint_ = setpoint; diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 4c32d92576..f5c7792782 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -20,7 +20,12 @@ void PIDClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_AUTO; + if (supports_heat_() && supports_cool_()) + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + else if (supports_cool_()) + this->mode = climate::CLIMATE_MODE_COOL; + else if (supports_heat_()) + this->mode = climate::CLIMATE_MODE_HEAT; this->target_temperature = this->default_target_temperature_; } } @@ -30,19 +35,25 @@ void PIDClimate::control(const climate::ClimateCall &call) { if (call.get_target_temperature().has_value()) this->target_temperature = *call.get_target_temperature(); - // If switching to non-auto mode, set output immediately - if (this->mode != climate::CLIMATE_MODE_AUTO) - this->handle_non_auto_mode_(); + // If switching to off mode, set output immediately + if (this->mode == climate::CLIMATE_MODE_OFF) + this->write_output_(0.0f); this->publish_state(); } climate::ClimateTraits PIDClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); traits.set_supports_two_point_target_temperature(false); - traits.set_supports_cool_mode(this->supports_cool_()); - traits.set_supports_heat_mode(this->supports_heat_()); + + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); + if (supports_cool_()) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_()) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_heat_() && supports_cool_()) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); + traits.set_supports_action(true); return traits; } @@ -87,23 +98,9 @@ void PIDClimate::write_output_(float value) { } this->pid_computed_callback_.call(); } -void PIDClimate::handle_non_auto_mode_() { - // in non-auto mode, switch directly to appropriate action - // - HEAT mode / COOL mode -> Output at ±100% - // - OFF mode -> Output at 0% - if (this->mode == climate::CLIMATE_MODE_HEAT) { - this->write_output_(1.0); - } else if (this->mode == climate::CLIMATE_MODE_COOL) { - this->write_output_(-1.0); - } else if (this->mode == climate::CLIMATE_MODE_OFF) { - this->write_output_(0.0); - } else { - assert(false); - } -} void PIDClimate::update_pid_() { float value; - if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) { // if any control parameters are nan, turn off all outputs value = 0.0; } else { @@ -121,15 +118,15 @@ void PIDClimate::update_pid_() { // keep autotuner instance so that subsequent dump_configs will print the long result message. } else { value = res.output; - if (mode != climate::CLIMATE_MODE_AUTO) { + if (mode != climate::CLIMATE_MODE_HEAT_COOL) { ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); } } } } - if (this->mode != climate::CLIMATE_MODE_AUTO) { - this->handle_non_auto_mode_(); + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->write_output_(0.0); } else { this->write_output_(value); } diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index f11d768867..ff301386b6 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -56,7 +56,6 @@ class PIDClimate : public climate::Climate, public Component { bool supports_heat_() const { return this->heat_output_ != nullptr; } void write_output_(float value); - void handle_non_auto_mode_(); /// The sensor used for getting the current temperature sensor::Sensor *sensor_; diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 4caad8dd8b..35e3eb9fc0 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace pid { @@ -23,9 +23,9 @@ struct PIDController { // i(t) := K_i * \int_{0}^{t} e(t) dt accumulated_integral_ += error * dt * ki; // constrain accumulated integral value - if (!isnan(min_integral) && accumulated_integral_ < min_integral) + if (!std::isnan(min_integral) && accumulated_integral_ < min_integral) accumulated_integral_ = min_integral; - if (!isnan(max_integral) && accumulated_integral_ > max_integral) + if (!std::isnan(max_integral) && accumulated_integral_ > max_integral) accumulated_integral_ = max_integral; integral_term = accumulated_integral_; diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index 61669d4716..d1007fcbc4 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_GAUGE, @@ -30,7 +29,10 @@ PID_CLIMATE_SENSOR_TYPES = { CONF_CLIMATE_ID = "climate_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_GAUGE, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py new file mode 100644 index 0000000000..20e4672125 --- /dev/null +++ b/esphome/components/pipsolar/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.components import uart + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@andreashergert1984"] +AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "output"] +MULTI_CONF = True + +CONF_PIPSOLAR_ID = "pipsolar_id" + +pipsolar_ns = cg.esphome_ns.namespace("pipsolar") +PipsolarComponent = pipsolar_ns.class_("Pipsolar", cg.Component) + +PIPSOLAR_COMPONENT_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_PIPSOLAR_ID): cv.use_id(PipsolarComponent), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PipsolarComponent)}) + .extend(cv.polling_component_schema("1s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py new file mode 100644 index 0000000000..5c6af3bffc --- /dev/null +++ b/esphome/components/pipsolar/binary_sensor/__init__.py @@ -0,0 +1,144 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_LOAD_STATUS = "load_status" +CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING = ( + "battery_voltage_to_steady_while_charging" +) +CONF_CHARGING_STATUS = "charging_status" +CONF_SCC_CHARGING_STATUS = "scc_charging_status" +CONF_AC_CHARGING_STATUS = "ac_charging_status" +CONF_CHARGING_TO_FLOATING_MODE = "charging_to_floating_mode" +CONF_SWITCH_ON = "switch_on" +CONF_DUSTPROOF_INSTALLED = "dustproof_installed" +CONF_SILENCE_BUZZER_OPEN_BUZZER = "silence_buzzer_open_buzzer" +CONF_OVERLOAD_BYPASS_FUNCTION = "overload_bypass_function" +CONF_LCD_ESCAPE_TO_DEFAULT = "lcd_escape_to_default" +CONF_OVERLOAD_RESTART_FUNCTION = "overload_restart_function" +CONF_OVER_TEMPERATURE_RESTART_FUNCTION = "over_temperature_restart_function" +CONF_BACKLIGHT_ON = "backlight_on" +CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT = "alarm_on_when_primary_source_interrupt" +CONF_FAULT_CODE_RECORD = "fault_code_record" +CONF_POWER_SAVING = "power_saving" + +CONF_WARNINGS_PRESENT = "warnings_present" +CONF_FAULTS_PRESENT = "faults_present" +CONF_WARNING_POWER_LOSS = "warning_power_loss" +CONF_FAULT_INVERTER_FAULT = "fault_inverter_fault" +CONF_FAULT_BUS_OVER = "fault_bus_over" +CONF_FAULT_BUS_UNDER = "fault_bus_under" +CONF_FAULT_BUS_SOFT_FAIL = "fault_bus_soft_fail" +CONF_WARNING_LINE_FAIL = "warning_line_fail" +CONF_FAULT_OPVSHORT = "fault_opvshort" +CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW = "fault_inverter_voltage_too_low" +CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH = "fault_inverter_voltage_too_high" +CONF_WARNING_OVER_TEMPERATURE = "warning_over_temperature" +CONF_WARNING_FAN_LOCK = "warning_fan_lock" +CONF_WARNING_BATTERY_VOLTAGE_HIGH = "warning_battery_voltage_high" +CONF_WARNING_BATTERY_LOW_ALARM = "warning_battery_low_alarm" +CONF_WARNING_BATTERY_UNDER_SHUTDOWN = "warning_battery_under_shutdown" +CONF_WARNING_BATTERY_DERATING = "warning_battery_derating" +CONF_WARNING_OVER_LOAD = "warning_over_load" +CONF_WARNING_EEPROM_FAILED = "warning_eeprom_failed" +CONF_FAULT_INVERTER_OVER_CURRENT = "fault_inverter_over_current" +CONF_FAULT_INVERTER_SOFT_FAILED = "fault_inverter_soft_failed" +CONF_FAULT_SELF_TEST_FAILED = "fault_self_test_failed" +CONF_FAULT_OP_DC_VOLTAGE_OVER = "fault_op_dc_voltage_over" +CONF_FAULT_BATTERY_OPEN = "fault_battery_open" +CONF_FAULT_CURRENT_SENSOR_FAILED = "fault_current_sensor_failed" +CONF_FAULT_BATTERY_SHORT = "fault_battery_short" +CONF_WARNING_POWER_LIMIT = "warning_power_limit" +CONF_WARNING_PV_VOLTAGE_HIGH = "warning_pv_voltage_high" +CONF_FAULT_MPPT_OVERLOAD = "fault_mppt_overload" +CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload" +CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge" +CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current" +CONF_FAULT_CODE = "fault_code" +CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy" +CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = ( + "warning_high_ac_input_during_bus_soft_start" +) +CONF_WARNING_BATTERY_EQUALIZATION = "warning_battery_equalization" + +TYPES = [ + CONF_ADD_SBU_PRIORITY_VERSION, + CONF_CONFIGURATION_STATUS, + CONF_SCC_FIRMWARE_VERSION, + CONF_LOAD_STATUS, + CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING, + CONF_CHARGING_STATUS, + CONF_SCC_CHARGING_STATUS, + CONF_AC_CHARGING_STATUS, + CONF_CHARGING_TO_FLOATING_MODE, + CONF_SWITCH_ON, + CONF_DUSTPROOF_INSTALLED, + CONF_SILENCE_BUZZER_OPEN_BUZZER, + CONF_OVERLOAD_BYPASS_FUNCTION, + CONF_LCD_ESCAPE_TO_DEFAULT, + CONF_OVERLOAD_RESTART_FUNCTION, + CONF_OVER_TEMPERATURE_RESTART_FUNCTION, + CONF_BACKLIGHT_ON, + CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT, + CONF_FAULT_CODE_RECORD, + CONF_POWER_SAVING, + CONF_WARNINGS_PRESENT, + CONF_FAULTS_PRESENT, + CONF_WARNING_POWER_LOSS, + CONF_FAULT_INVERTER_FAULT, + CONF_FAULT_BUS_OVER, + CONF_FAULT_BUS_UNDER, + CONF_FAULT_BUS_SOFT_FAIL, + CONF_WARNING_LINE_FAIL, + CONF_FAULT_OPVSHORT, + CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW, + CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH, + CONF_WARNING_OVER_TEMPERATURE, + CONF_WARNING_FAN_LOCK, + CONF_WARNING_BATTERY_VOLTAGE_HIGH, + CONF_WARNING_BATTERY_LOW_ALARM, + CONF_WARNING_BATTERY_UNDER_SHUTDOWN, + CONF_WARNING_BATTERY_DERATING, + CONF_WARNING_OVER_LOAD, + CONF_WARNING_EEPROM_FAILED, + CONF_FAULT_INVERTER_OVER_CURRENT, + CONF_FAULT_INVERTER_SOFT_FAILED, + CONF_FAULT_SELF_TEST_FAILED, + CONF_FAULT_OP_DC_VOLTAGE_OVER, + CONF_FAULT_BATTERY_OPEN, + CONF_FAULT_CURRENT_SENSOR_FAILED, + CONF_FAULT_BATTERY_SHORT, + CONF_WARNING_POWER_LIMIT, + CONF_WARNING_PV_VOLTAGE_HIGH, + CONF_FAULT_MPPT_OVERLOAD, + CONF_WARNING_MPPT_OVERLOAD, + CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE, + CONF_FAULT_DC_DC_OVER_CURRENT, + CONF_FAULT_CODE, + CONF_WARNUNG_LOW_PV_ENERGY, + CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, + CONF_WARNING_BATTERY_EQUALIZATION, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): binary_sensor.BINARY_SENSOR_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + for type in TYPES: + if type in config: + conf = config[type] + sens = cg.new_Pvariable(conf[CONF_ID]) + await binary_sensor.register_binary_sensor(sens, conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py new file mode 100644 index 0000000000..b518d485e7 --- /dev/null +++ b/esphome/components/pipsolar/output/__init__.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import output +from esphome.const import CONF_ID, CONF_VALUE +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID, pipsolar_ns + +DEPENDENCIES = ["pipsolar"] + +PipsolarOutput = pipsolar_ns.class_("PipsolarOutput", output.FloatOutput) +SetOutputAction = pipsolar_ns.class_("SetOutputAction", automation.Action) + +CONF_POSSIBLE_VALUES = "possible_values" + +# 3.11 PCVV: Setting battery C.V. (constant voltage) charging voltage 48.0V ~ 58.4V for 48V unit +# battery_bulk_voltage; +# battery_recharge_voltage; 12V unit: 11V/11.3V/11.5V/11.8V/12V/12.3V/12.5V/12.8V +# 24V unit: 22V/22.5V/23V/23.5V/24V/24.5V/25V/25.5V +# 48V unit: 44V/45V/46V/47V/48V/49V/50V/51V +# battery_under_voltage; 40.0V ~ 48.0V for 48V unit +# battery_float_voltage; 48.0V ~ 58.4V for 48V unit +# battery_type; 00 for AGM, 01 for Flooded battery +# current_max_ac_charging_current; +# output_source_priority; 00 / 01 / 02 +# charger_source_priority; For HS: 00 for utility first, 01 for solar first, 02 for solar and utility, 03 for only solar charging +# For MS/MSX: 00 for utility first, 01 for solar first, 03 for only solar charging +# battery_redischarge_voltage; 12V unit: 00.0V12V/12.3V/12.5V/12.8V/13V/13.3V/13.5V/13.8V/14V/14.3V/14.5 +# 24V unit: 00.0V/24V/24.5V/25V/25.5V/26V/26.5V/27V/27.5V/28V/28.5V/29V +# 48V unit: 00.0V48V/49V/50V/51V/52V/53V/54V/55V/56V/57V/58V + +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" + +TYPES = { + CONF_BATTERY_RECHARGE_VOLTAGE: ( + [44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0], + "PBCV%02.1f", + ), + CONF_BATTERY_UNDER_VOLTAGE: ( + [40.0, 40.1, 42, 43, 44, 45, 46, 47, 48.0], + "PSDV%02.1f", + ), + CONF_BATTERY_FLOAT_VOLTAGE: ([48.0, 49.0, 50.0, 51.0], "PBFT%02.1f"), + CONF_BATTERY_TYPE: ([0, 1, 2], "PBT%02.0f"), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: ([2, 10, 20], "MUCHGC0%02.0f"), + CONF_CURRENT_MAX_CHARGING_CURRENT: ([10, 20, 30, 40], "MCHGC0%02.0f"), + CONF_OUTPUT_SOURCE_PRIORITY: ([0, 1, 2], "POP%02.0f"), + CONF_CHARGER_SOURCE_PRIORITY: ([0, 1, 2, 3], "PCP%02.0f"), + CONF_BATTERY_REDISCHARGE_VOLTAGE: ( + [0, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58], + "PBDV%02.1f", + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(PipsolarOutput), + cv.Optional(CONF_POSSIBLE_VALUES, default=values): cv.All( + cv.ensure_list(cv.positive_float), cv.Length(min=1) + ), + } + ) + for type, (values, _) in TYPES.items() + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (_, command) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await output.register_output(var, conf) + cg.add(var.set_parent(paren)) + cg.add(var.set_set_command(command)) + if (CONF_POSSIBLE_VALUES) in conf: + cg.add(var.set_possible_values(conf[CONF_POSSIBLE_VALUES])) + + +@automation.register_action( + "output.pipsolar.set_level", + SetOutputAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(CONF_ID), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), + } + ), +) +def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_level(template_)) + yield var diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp new file mode 100644 index 0000000000..b843f1f3e6 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -0,0 +1,22 @@ +#include "pipsolar_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.output"; + +void PipsolarOutput::write_state(float state) { + char tmp[10]; + sprintf(tmp, this->set_command_.c_str(), state); + + if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) { + ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state); + this->parent_->switch_command(std::string(tmp)); + } else { + ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); + } +} +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h new file mode 100644 index 0000000000..fe783cf034 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -0,0 +1,40 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +class Pipsolar; + +class PipsolarOutput : public output::FloatOutput { + public: + PipsolarOutput() {} + void set_parent(Pipsolar *parent) { this->parent_ = parent; } + void set_set_command(const std::string &command) { this->set_command_ = command; }; + void set_possible_values(std::vector possible_values) { this->possible_values_ = std::move(possible_values); } + void set_value(float value) { this->write_state(value); }; + + protected: + void write_state(float state) override; + std::string set_command_; + Pipsolar *parent_; + std::vector possible_values_; +}; + +template class SetOutputAction : public Action { + public: + SetOutputAction(PipsolarOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, level) + + void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + + protected: + PipsolarOutput *output_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp new file mode 100644 index 0000000000..7dbbd798ad --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -0,0 +1,922 @@ +#include "pipsolar.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar"; + +void Pipsolar::setup() { + this->state_ = STATE_IDLE; + this->command_start_millis_ = 0; +} + +void Pipsolar::empty_uart_buffer_() { + uint8_t byte; + while (this->available()) { + this->read_byte(&byte); + } +} + +void Pipsolar::loop() { + // Read message + if (this->state_ == STATE_IDLE) { + this->empty_uart_buffer_(); + switch (this->send_next_command_()) { + case 0: + // no command send (empty queue) time to poll + if (millis() - this->last_poll_ > this->update_interval_) { + this->send_next_poll_(); + this->last_poll_ = millis(); + } + return; + break; + case 1: + // command send + return; + break; + } + } + if (this->state_ == STATE_COMMAND_COMPLETE) { + if (this->check_incoming_length_(4)) { + ESP_LOGD(TAG, "response length for command OK"); + if (this->check_incoming_crc_()) { + // crc ok + if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { + ESP_LOGD(TAG, "command successful"); + } else { + ESP_LOGD(TAG, "command not successful"); + } + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + + } else { + // crc failed + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } else { + ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", + this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_POLL_DECODED) { + std::string mode; + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + if (this->grid_rating_voltage_) { + this->grid_rating_voltage_->publish_state(value_grid_rating_voltage_); + } + if (this->grid_rating_current_) { + this->grid_rating_current_->publish_state(value_grid_rating_current_); + } + if (this->ac_output_rating_voltage_) { + this->ac_output_rating_voltage_->publish_state(value_ac_output_rating_voltage_); + } + if (this->ac_output_rating_frequency_) { + this->ac_output_rating_frequency_->publish_state(value_ac_output_rating_frequency_); + } + if (this->ac_output_rating_current_) { + this->ac_output_rating_current_->publish_state(value_ac_output_rating_current_); + } + if (this->ac_output_rating_apparent_power_) { + this->ac_output_rating_apparent_power_->publish_state(value_ac_output_rating_apparent_power_); + } + if (this->ac_output_rating_active_power_) { + this->ac_output_rating_active_power_->publish_state(value_ac_output_rating_active_power_); + } + if (this->battery_rating_voltage_) { + this->battery_rating_voltage_->publish_state(value_battery_rating_voltage_); + } + if (this->battery_recharge_voltage_) { + this->battery_recharge_voltage_->publish_state(value_battery_recharge_voltage_); + } + if (this->battery_under_voltage_) { + this->battery_under_voltage_->publish_state(value_battery_under_voltage_); + } + if (this->battery_bulk_voltage_) { + this->battery_bulk_voltage_->publish_state(value_battery_bulk_voltage_); + } + if (this->battery_float_voltage_) { + this->battery_float_voltage_->publish_state(value_battery_float_voltage_); + } + if (this->battery_type_) { + this->battery_type_->publish_state(value_battery_type_); + } + if (this->current_max_ac_charging_current_) { + this->current_max_ac_charging_current_->publish_state(value_current_max_ac_charging_current_); + } + if (this->current_max_charging_current_) { + this->current_max_charging_current_->publish_state(value_current_max_charging_current_); + } + if (this->input_voltage_range_) { + this->input_voltage_range_->publish_state(value_input_voltage_range_); + } + // special for input voltage range switch + if (this->input_voltage_range_switch_) { + this->input_voltage_range_switch_->publish_state(value_input_voltage_range_ == 1); + } + if (this->output_source_priority_) { + this->output_source_priority_->publish_state(value_output_source_priority_); + } + // special for output source priority switches + if (this->output_source_priority_utility_switch_) { + this->output_source_priority_utility_switch_->publish_state(value_output_source_priority_ == 0); + } + if (this->output_source_priority_solar_switch_) { + this->output_source_priority_solar_switch_->publish_state(value_output_source_priority_ == 1); + } + if (this->output_source_priority_battery_switch_) { + this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); + } + if (this->charger_source_priority_) { + this->charger_source_priority_->publish_state(value_charger_source_priority_); + } + if (this->parallel_max_num_) { + this->parallel_max_num_->publish_state(value_parallel_max_num_); + } + if (this->machine_type_) { + this->machine_type_->publish_state(value_machine_type_); + } + if (this->topology_) { + this->topology_->publish_state(value_topology_); + } + if (this->output_mode_) { + this->output_mode_->publish_state(value_output_mode_); + } + if (this->battery_redischarge_voltage_) { + this->battery_redischarge_voltage_->publish_state(value_battery_redischarge_voltage_); + } + if (this->pv_ok_condition_for_parallel_) { + this->pv_ok_condition_for_parallel_->publish_state(value_pv_ok_condition_for_parallel_); + } + // special for pv ok condition switch + if (this->pv_ok_condition_for_parallel_switch_) { + this->pv_ok_condition_for_parallel_switch_->publish_state(value_pv_ok_condition_for_parallel_ == 1); + } + if (this->pv_power_balance_) { + this->pv_power_balance_->publish_state(value_pv_power_balance_ == 1); + } + // special for power balance switch + if (this->pv_power_balance_switch_) { + this->pv_power_balance_switch_->publish_state(value_pv_power_balance_ == 1); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIGS: + if (this->grid_voltage_) { + this->grid_voltage_->publish_state(value_grid_voltage_); + } + if (this->grid_frequency_) { + this->grid_frequency_->publish_state(value_grid_frequency_); + } + if (this->ac_output_voltage_) { + this->ac_output_voltage_->publish_state(value_ac_output_voltage_); + } + if (this->ac_output_frequency_) { + this->ac_output_frequency_->publish_state(value_ac_output_frequency_); + } + if (this->ac_output_apparent_power_) { + this->ac_output_apparent_power_->publish_state(value_ac_output_apparent_power_); + } + if (this->ac_output_active_power_) { + this->ac_output_active_power_->publish_state(value_ac_output_active_power_); + } + if (this->output_load_percent_) { + this->output_load_percent_->publish_state(value_output_load_percent_); + } + if (this->bus_voltage_) { + this->bus_voltage_->publish_state(value_bus_voltage_); + } + if (this->battery_voltage_) { + this->battery_voltage_->publish_state(value_battery_voltage_); + } + if (this->battery_charging_current_) { + this->battery_charging_current_->publish_state(value_battery_charging_current_); + } + if (this->battery_capacity_percent_) { + this->battery_capacity_percent_->publish_state(value_battery_capacity_percent_); + } + if (this->inverter_heat_sink_temperature_) { + this->inverter_heat_sink_temperature_->publish_state(value_inverter_heat_sink_temperature_); + } + if (this->pv_input_current_for_battery_) { + this->pv_input_current_for_battery_->publish_state(value_pv_input_current_for_battery_); + } + if (this->pv_input_voltage_) { + this->pv_input_voltage_->publish_state(value_pv_input_voltage_); + } + if (this->battery_voltage_scc_) { + this->battery_voltage_scc_->publish_state(value_battery_voltage_scc_); + } + if (this->battery_discharge_current_) { + this->battery_discharge_current_->publish_state(value_battery_discharge_current_); + } + if (this->add_sbu_priority_version_) { + this->add_sbu_priority_version_->publish_state(value_add_sbu_priority_version_); + } + if (this->configuration_status_) { + this->configuration_status_->publish_state(value_configuration_status_); + } + if (this->scc_firmware_version_) { + this->scc_firmware_version_->publish_state(value_scc_firmware_version_); + } + if (this->load_status_) { + this->load_status_->publish_state(value_load_status_); + } + if (this->battery_voltage_to_steady_while_charging_) { + this->battery_voltage_to_steady_while_charging_->publish_state( + value_battery_voltage_to_steady_while_charging_); + } + if (this->charging_status_) { + this->charging_status_->publish_state(value_charging_status_); + } + if (this->scc_charging_status_) { + this->scc_charging_status_->publish_state(value_scc_charging_status_); + } + if (this->ac_charging_status_) { + this->ac_charging_status_->publish_state(value_ac_charging_status_); + } + if (this->battery_voltage_offset_for_fans_on_) { + this->battery_voltage_offset_for_fans_on_->publish_state(value_battery_voltage_offset_for_fans_on_ / 10.0f); + } //.1 scale + if (this->eeprom_version_) { + this->eeprom_version_->publish_state(value_eeprom_version_); + } + if (this->pv_charging_power_) { + this->pv_charging_power_->publish_state(value_pv_charging_power_); + } + if (this->charging_to_floating_mode_) { + this->charging_to_floating_mode_->publish_state(value_charging_to_floating_mode_); + } + if (this->switch_on_) { + this->switch_on_->publish_state(value_switch_on_); + } + if (this->dustproof_installed_) { + this->dustproof_installed_->publish_state(value_dustproof_installed_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QMOD: + if (this->device_mode_) { + mode = value_device_mode_; + this->device_mode_->publish_state(mode); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QFLAG: + if (this->silence_buzzer_open_buzzer_) { + this->silence_buzzer_open_buzzer_->publish_state(value_silence_buzzer_open_buzzer_); + } + if (this->overload_bypass_function_) { + this->overload_bypass_function_->publish_state(value_overload_bypass_function_); + } + if (this->lcd_escape_to_default_) { + this->lcd_escape_to_default_->publish_state(value_lcd_escape_to_default_); + } + if (this->overload_restart_function_) { + this->overload_restart_function_->publish_state(value_overload_restart_function_); + } + if (this->over_temperature_restart_function_) { + this->over_temperature_restart_function_->publish_state(value_over_temperature_restart_function_); + } + if (this->backlight_on_) { + this->backlight_on_->publish_state(value_backlight_on_); + } + if (this->alarm_on_when_primary_source_interrupt_) { + this->alarm_on_when_primary_source_interrupt_->publish_state(value_alarm_on_when_primary_source_interrupt_); + } + if (this->fault_code_record_) { + this->fault_code_record_->publish_state(value_fault_code_record_); + } + if (this->power_saving_) { + this->power_saving_->publish_state(value_power_saving_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIWS: + if (this->warnings_present_) { + this->warnings_present_->publish_state(value_warnings_present_); + } + if (this->faults_present_) { + this->faults_present_->publish_state(value_faults_present_); + } + if (this->warning_power_loss_) { + this->warning_power_loss_->publish_state(value_warning_power_loss_); + } + if (this->fault_inverter_fault_) { + this->fault_inverter_fault_->publish_state(value_fault_inverter_fault_); + } + if (this->fault_bus_over_) { + this->fault_bus_over_->publish_state(value_fault_bus_over_); + } + if (this->fault_bus_under_) { + this->fault_bus_under_->publish_state(value_fault_bus_under_); + } + if (this->fault_bus_soft_fail_) { + this->fault_bus_soft_fail_->publish_state(value_fault_bus_soft_fail_); + } + if (this->warning_line_fail_) { + this->warning_line_fail_->publish_state(value_warning_line_fail_); + } + if (this->fault_opvshort_) { + this->fault_opvshort_->publish_state(value_fault_opvshort_); + } + if (this->fault_inverter_voltage_too_low_) { + this->fault_inverter_voltage_too_low_->publish_state(value_fault_inverter_voltage_too_low_); + } + if (this->fault_inverter_voltage_too_high_) { + this->fault_inverter_voltage_too_high_->publish_state(value_fault_inverter_voltage_too_high_); + } + if (this->warning_over_temperature_) { + this->warning_over_temperature_->publish_state(value_warning_over_temperature_); + } + if (this->warning_fan_lock_) { + this->warning_fan_lock_->publish_state(value_warning_fan_lock_); + } + if (this->warning_battery_voltage_high_) { + this->warning_battery_voltage_high_->publish_state(value_warning_battery_voltage_high_); + } + if (this->warning_battery_low_alarm_) { + this->warning_battery_low_alarm_->publish_state(value_warning_battery_low_alarm_); + } + if (this->warning_battery_under_shutdown_) { + this->warning_battery_under_shutdown_->publish_state(value_warning_battery_under_shutdown_); + } + if (this->warning_battery_derating_) { + this->warning_battery_derating_->publish_state(value_warning_battery_derating_); + } + if (this->warning_over_load_) { + this->warning_over_load_->publish_state(value_warning_over_load_); + } + if (this->warning_eeprom_failed_) { + this->warning_eeprom_failed_->publish_state(value_warning_eeprom_failed_); + } + if (this->fault_inverter_over_current_) { + this->fault_inverter_over_current_->publish_state(value_fault_inverter_over_current_); + } + if (this->fault_inverter_soft_failed_) { + this->fault_inverter_soft_failed_->publish_state(value_fault_inverter_soft_failed_); + } + if (this->fault_self_test_failed_) { + this->fault_self_test_failed_->publish_state(value_fault_self_test_failed_); + } + if (this->fault_op_dc_voltage_over_) { + this->fault_op_dc_voltage_over_->publish_state(value_fault_op_dc_voltage_over_); + } + if (this->fault_battery_open_) { + this->fault_battery_open_->publish_state(value_fault_battery_open_); + } + if (this->fault_current_sensor_failed_) { + this->fault_current_sensor_failed_->publish_state(value_fault_current_sensor_failed_); + } + if (this->fault_battery_short_) { + this->fault_battery_short_->publish_state(value_fault_battery_short_); + } + if (this->warning_power_limit_) { + this->warning_power_limit_->publish_state(value_warning_power_limit_); + } + if (this->warning_pv_voltage_high_) { + this->warning_pv_voltage_high_->publish_state(value_warning_pv_voltage_high_); + } + if (this->fault_mppt_overload_) { + this->fault_mppt_overload_->publish_state(value_fault_mppt_overload_); + } + if (this->warning_mppt_overload_) { + this->warning_mppt_overload_->publish_state(value_warning_mppt_overload_); + } + if (this->warning_battery_too_low_to_charge_) { + this->warning_battery_too_low_to_charge_->publish_state(value_warning_battery_too_low_to_charge_); + } + if (this->fault_dc_dc_over_current_) { + this->fault_dc_dc_over_current_->publish_state(value_fault_dc_dc_over_current_); + } + if (this->fault_code_) { + this->fault_code_->publish_state(value_fault_code_); + } + if (this->warnung_low_pv_energy_) { + this->warnung_low_pv_energy_->publish_state(value_warnung_low_pv_energy_); + } + if (this->warning_high_ac_input_during_bus_soft_start_) { + this->warning_high_ac_input_during_bus_soft_start_->publish_state( + value_warning_high_ac_input_during_bus_soft_start_); + } + if (this->warning_battery_equalization_) { + this->warning_battery_equalization_->publish_state(value_warning_battery_equalization_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QT: + this->state_ = STATE_IDLE; + break; + case POLLING_QMN: + this->state_ = STATE_IDLE; + break; + } + } + + if (this->state_ == STATE_POLL_CHECKED) { + bool enabled = true; + std::string fc; + char tmp[PIPSOLAR_READ_BUFFER_LENGTH]; + sprintf(tmp, "%s", this->read_buffer_); + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + ESP_LOGD(TAG, "Decode QPIRI"); + sscanf(tmp, "(%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d %d %d %d %d %f %d %d", // NOLINT + &value_grid_rating_voltage_, &value_grid_rating_current_, &value_ac_output_rating_voltage_, // NOLINT + &value_ac_output_rating_frequency_, &value_ac_output_rating_current_, // NOLINT + &value_ac_output_rating_apparent_power_, &value_ac_output_rating_active_power_, // NOLINT + &value_battery_rating_voltage_, &value_battery_recharge_voltage_, // NOLINT + &value_battery_under_voltage_, &value_battery_bulk_voltage_, &value_battery_float_voltage_, // NOLINT + &value_battery_type_, &value_current_max_ac_charging_current_, // NOLINT + &value_current_max_charging_current_, &value_input_voltage_range_, // NOLINT + &value_output_source_priority_, &value_charger_source_priority_, &value_parallel_max_num_, // NOLINT + &value_machine_type_, &value_topology_, &value_output_mode_, // NOLINT + &value_battery_redischarge_voltage_, &value_pv_ok_condition_for_parallel_, // NOLINT + &value_pv_power_balance_); // NOLINT + if (this->last_qpiri_) { + this->last_qpiri_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIGS: + ESP_LOGD(TAG, "Decode QPIGS"); + sscanf( // NOLINT + tmp, // NOLINT + "(%f %f %f %f %d %d %d %d %f %d %d %d %d %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT + &value_grid_voltage_, &value_grid_frequency_, &value_ac_output_voltage_, // NOLINT + &value_ac_output_frequency_, // NOLINT + &value_ac_output_apparent_power_, &value_ac_output_active_power_, &value_output_load_percent_, // NOLINT + &value_bus_voltage_, &value_battery_voltage_, &value_battery_charging_current_, // NOLINT + &value_battery_capacity_percent_, &value_inverter_heat_sink_temperature_, // NOLINT + &value_pv_input_current_for_battery_, &value_pv_input_voltage_, &value_battery_voltage_scc_, // NOLINT + &value_battery_discharge_current_, &value_add_sbu_priority_version_, // NOLINT + &value_configuration_status_, &value_scc_firmware_version_, &value_load_status_, // NOLINT + &value_battery_voltage_to_steady_while_charging_, &value_charging_status_, // NOLINT + &value_scc_charging_status_, &value_ac_charging_status_, // NOLINT + &value_battery_voltage_offset_for_fans_on_, &value_eeprom_version_, &value_pv_charging_power_, // NOLINT + &value_charging_to_floating_mode_, &value_switch_on_, // NOLINT + &value_dustproof_installed_); // NOLINT + if (this->last_qpigs_) { + this->last_qpigs_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMOD: + ESP_LOGD(TAG, "Decode QMOD"); + this->value_device_mode_ = char(this->read_buffer_[1]); + if (this->last_qmod_) { + this->last_qmod_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QFLAG: + ESP_LOGD(TAG, "Decode QFLAG"); + // result like:"(EbkuvxzDajy" + // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value + for (int i = 1; i < strlen(tmp); i++) { + switch (tmp[i]) { + case 'E': + enabled = true; + break; + case 'D': + enabled = false; + break; + case 'a': + this->value_silence_buzzer_open_buzzer_ = enabled; + break; + case 'b': + this->value_overload_bypass_function_ = enabled; + break; + case 'k': + this->value_lcd_escape_to_default_ = enabled; + break; + case 'u': + this->value_overload_restart_function_ = enabled; + break; + case 'v': + this->value_over_temperature_restart_function_ = enabled; + break; + case 'x': + this->value_backlight_on_ = enabled; + break; + case 'y': + this->value_alarm_on_when_primary_source_interrupt_ = enabled; + break; + case 'z': + this->value_fault_code_record_ = enabled; + break; + case 'j': + this->value_power_saving_ = enabled; + break; + } + } + if (this->last_qflag_) { + this->last_qflag_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIWS: + ESP_LOGD(TAG, "Decode QPIWS"); + // '(00000000000000000000000000000000' + // iterate over all available flag (as not all models have all flags, but at least in the same order) + this->value_warnings_present_ = false; + this->value_faults_present_ = true; + + for (int i = 1; i < strlen(tmp); i++) { + enabled = tmp[i] == '1'; + switch (i) { + case 1: + this->value_warning_power_loss_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 2: + this->value_fault_inverter_fault_ = enabled; + this->value_faults_present_ += enabled; + break; + case 3: + this->value_fault_bus_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 4: + this->value_fault_bus_under_ = enabled; + this->value_faults_present_ += enabled; + break; + case 5: + this->value_fault_bus_soft_fail_ = enabled; + this->value_faults_present_ += enabled; + break; + case 6: + this->value_warning_line_fail_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 7: + this->value_fault_opvshort_ = enabled; + this->value_faults_present_ += enabled; + break; + case 8: + this->value_fault_inverter_voltage_too_low_ = enabled; + this->value_faults_present_ += enabled; + break; + case 9: + this->value_fault_inverter_voltage_too_high_ = enabled; + this->value_faults_present_ += enabled; + break; + case 10: + this->value_warning_over_temperature_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 11: + this->value_warning_fan_lock_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 12: + this->value_warning_battery_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 13: + this->value_warning_battery_low_alarm_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 15: + this->value_warning_battery_under_shutdown_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 16: + this->value_warning_battery_derating_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 17: + this->value_warning_over_load_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 18: + this->value_warning_eeprom_failed_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 19: + this->value_fault_inverter_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 20: + this->value_fault_inverter_soft_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 21: + this->value_fault_self_test_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 22: + this->value_fault_op_dc_voltage_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 23: + this->value_fault_battery_open_ = enabled; + this->value_faults_present_ += enabled; + break; + case 24: + this->value_fault_current_sensor_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 25: + this->value_fault_battery_short_ = enabled; + this->value_faults_present_ += enabled; + break; + case 26: + this->value_warning_power_limit_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 27: + this->value_warning_pv_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 28: + this->value_fault_mppt_overload_ = enabled; + this->value_faults_present_ += enabled; + break; + case 29: + this->value_warning_mppt_overload_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 30: + this->value_warning_battery_too_low_to_charge_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 31: + this->value_fault_dc_dc_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 32: + fc = tmp[i]; + fc += tmp[i + 1]; + this->value_fault_code_ = strtol(fc.c_str(), nullptr, 10); + break; + case 34: + this->value_warnung_low_pv_energy_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 35: + this->value_warning_high_ac_input_during_bus_soft_start_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 36: + this->value_warning_battery_equalization_ = enabled; + this->value_warnings_present_ += enabled; + break; + } + } + if (this->last_qpiws_) { + this->last_qpiws_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QT: + ESP_LOGD(TAG, "Decode QT"); + if (this->last_qt_) { + this->last_qt_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMN: + ESP_LOGD(TAG, "Decode QMN"); + if (this->last_qmn_) { + this->last_qmn_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + default: + this->state_ = STATE_IDLE; + break; + } + return; + } + + if (this->state_ == STATE_POLL_COMPLETE) { + if (this->check_incoming_crc_()) { + if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && + this->read_buffer_[3] == 'K') { + this->state_ = STATE_IDLE; + return; + } + // crc ok + this->state_ = STATE_POLL_CHECKED; + return; + } else { + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == PIPSOLAR_READ_BUFFER_LENGTH) { + this->read_pos_ = 0; + this->empty_uart_buffer_(); + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + + // end of answer + if (byte == 0x0D) { + this->read_buffer_[this->read_pos_] = 0; + this->empty_uart_buffer_(); + if (this->state_ == STATE_POLL) { + this->state_ = STATE_POLL_COMPLETE; + } + if (this->state_ == STATE_COMMAND) { + this->state_ = STATE_COMMAND_COMPLETE; + } + } + } // available + } + if (this->state_ == STATE_COMMAND) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + this->command_start_millis_ = millis(); + ESP_LOGD(TAG, "timeout command from queue: %s", command); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + return; + } else { + } + } + if (this->state_ == STATE_POLL) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + ESP_LOGD(TAG, "timeout command to poll: %s", this->used_polling_commands_[this->last_polling_command_].command); + this->state_ = STATE_IDLE; + } else { + } + } +} + +uint8_t Pipsolar::check_incoming_length_(uint8_t length) { + if (this->read_pos_ - 3 == length) { + return 1; + } + return 0; +} + +uint8_t Pipsolar::check_incoming_crc_() { + uint16_t crc16; + crc16 = calc_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]) { + ESP_LOGD(TAG, "CRC OK"); + read_buffer_[read_pos_ - 1] = 0; + read_buffer_[read_pos_ - 2] = 0; + read_buffer_[read_pos_ - 3] = 0; + return 1; + } + ESP_LOGD(TAG, "CRC NOK expected: %X %X but got: %X %X", ((uint8_t)((crc16) >> 8)), ((uint8_t)((crc16) &0xff)), + read_buffer_[read_pos_ - 3], read_buffer_[read_pos_ - 2]); + return 0; +} + +// send next command used +uint8_t Pipsolar::send_next_command_() { + uint16_t crc16; + if (this->command_queue_[this->command_queue_position_].length() != 0) { + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + uint8_t byte_command[16]; + uint8_t length = this->command_queue_[this->command_queue_position_].length(); + for (uint8_t i = 0; i < length; i++) { + byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); + } + this->state_ = STATE_COMMAND; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_crc_(byte_command, length); + this->write_str(command); + // checksum + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length); + return 1; + } + return 0; +} + +void Pipsolar::send_next_poll_() { + uint16_t crc16; + this->last_polling_command_ = (this->last_polling_command_ + 1) % 15; + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + this->last_polling_command_ = 0; + } + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + // no command specified + return; + } + this->state_ = STATE_POLL; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_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 + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending polling command : %s with length %d", + this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); +} + +void Pipsolar::queue_command_(const char *command, uint8_t length) { + uint8_t next_position = command_queue_position_; + for (uint8_t i = 0; i < COMMAND_QUEUE_LENGTH; i++) { + uint8_t testposition = (next_position + i) % COMMAND_QUEUE_LENGTH; + if (command_queue_[testposition].length() == 0) { + command_queue_[testposition] = command; + ESP_LOGD(TAG, "Command queued successfully: %s with length %u at position %d", command, + command_queue_[testposition].length(), testposition); + return; + } + } + ESP_LOGD(TAG, "Command queue full dropping command: %s", command); +} + +void Pipsolar::switch_command(const std::string &command) { + ESP_LOGD(TAG, "got command: %s", command.c_str()); + queue_command_(command.c_str(), command.length()); +} +void Pipsolar::dump_config() { + ESP_LOGCONFIG(TAG, "Pipsolar:"); + ESP_LOGCONFIG(TAG, "used commands:"); + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length != 0) { + ESP_LOGCONFIG(TAG, "%s", used_polling_command.command); + } + } +} +void Pipsolar::update() {} + +void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) { + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length == strlen(command)) { + uint8_t len = strlen(command); + if (memcmp(used_polling_command.command, command, len) == 0) { + return; + } + } + if (used_polling_command.length == 0) { + size_t length = strlen(command) + 1; + const char *beg = command; + const char *end = command + length; + used_polling_command.command = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) + size_t i = 0; + for (; beg != end; ++beg, ++i) { + used_polling_command.command[i] = (uint8_t)(*beg); + } + used_polling_command.errors = 0; + used_polling_command.identifier = polling_command; + used_polling_command.length = length - 1; + return; + } + } +} + +uint16_t Pipsolar::calc_crc_(uint8_t *msg, int n) { + // Initial value. xmodem uses 0xFFFF but this example + // requires an initial value of zero. + uint16_t x = 0; + while (n--) { + x = crc_xmodem_update_(x, (uint16_t) *msg++); + } + return (x); +} + +// See bottom of this page: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html +// Polynomial: x^16 + x^12 + x^5 + 1 (0x1021) +uint16_t Pipsolar::crc_xmodem_update_(uint16_t crc, uint8_t data) { + int i; + crc = crc ^ ((uint16_t) data << 8); + for (i = 0; i < 8; i++) { + if (crc & 0x8000) + crc = (crc << 1) ^ 0x1021; //(polynomial = 0x1021) + else + crc <<= 1; + } + return crc; +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h new file mode 100644 index 0000000000..fe2a80d1d5 --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.h @@ -0,0 +1,223 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +enum ENUMPollingCommand { + POLLING_QPIRI = 0, + POLLING_QPIGS = 1, + POLLING_QMOD = 2, + POLLING_QFLAG = 3, + POLLING_QPIWS = 4, + POLLING_QT = 5, + POLLING_QMN = 6, +}; +struct PollingCommand { + uint8_t *command; + uint8_t length = 0; + uint8_t errors; + ENUMPollingCommand identifier; +}; + +#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ + protected: \ + value_type value_##name##_; \ + PIPSOLAR_ENTITY_(type, name, polling_command) + +#define PIPSOLAR_ENTITY_(type, name, polling_command) \ + protected: \ + type *name##_{}; /* NOLINT */ \ +\ + public: \ + void set_##name(type *name) { /* NOLINT */ \ + this->name##_ = name; \ + this->add_polling_command_(#polling_command, POLLING_##polling_command); \ + } + +#define PIPSOLAR_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type) +#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command) +#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type) +#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type) +#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command) + +class Pipsolar : public uart::UARTDevice, public PollingComponent { + // QPIGS values + PIPSOLAR_SENSOR(grid_voltage, QPIGS, float) + PIPSOLAR_SENSOR(grid_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int) + PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int) + PIPSOLAR_SENSOR(output_load_percent, QPIGS, int) + PIPSOLAR_SENSOR(bus_voltage, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) + PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) + PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) + PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale + PIPSOLAR_SENSOR(eeprom_version, QPIGS, int) + PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int) + + // QPIRI values + PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int) + PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int) + PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_type, QPIRI, int) + PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int) + PIPSOLAR_SENSOR(output_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int) + PIPSOLAR_SENSOR(machine_type, QPIRI, int) + PIPSOLAR_SENSOR(topology, QPIRI, int) + PIPSOLAR_SENSOR(output_mode, QPIRI, int) + PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int) + PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int) + + // QMOD values + PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char) + + // QFLAG values + PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int) + + // QPIWS values + PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int) + PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool) + + PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) + PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI) + PIPSOLAR_TEXT_SENSOR(last_qmod, QMOD) + PIPSOLAR_TEXT_SENSOR(last_qflag, QFLAG) + PIPSOLAR_TEXT_SENSOR(last_qpiws, QPIWS) + PIPSOLAR_TEXT_SENSOR(last_qt, QT) + PIPSOLAR_TEXT_SENSOR(last_qmn, QMN) + + 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(input_voltage_range_switch, QPIRI) + PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) + PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) + + void switch_command(const std::string &command); + void setup() override; + void loop() override; + void dump_config() override; + void update() override; + + protected: + static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length + static const size_t COMMAND_QUEUE_LENGTH = 10; + static const size_t COMMAND_TIMEOUT = 5000; + uint32_t last_poll_ = 0; + void add_polling_command_(const char *command, ENUMPollingCommand polling_command); + void empty_uart_buffer_(); + uint8_t check_incoming_crc_(); + uint8_t check_incoming_length_(uint8_t length); + uint16_t calc_crc_(uint8_t *msg, int n); + uint16_t crc_xmodem_update_(uint16_t crc, uint8_t data); + uint8_t send_next_command_(); + void send_next_poll_(); + void queue_command_(const char *command, uint8_t length); + std::string command_queue_[COMMAND_QUEUE_LENGTH]; + uint8_t command_queue_position_ = 0; + uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + uint32_t command_start_millis_ = 0; + uint8_t state_; + enum State { + STATE_IDLE = 0, + STATE_POLL = 1, + STATE_COMMAND = 2, + STATE_POLL_COMPLETE = 3, + STATE_COMMAND_COMPLETE = 4, + STATE_POLL_CHECKED = 5, + STATE_POLL_DECODED = 6, + }; + + uint8_t last_polling_command_ = 0; + PollingCommand used_polling_commands_[15]; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py new file mode 100644 index 0000000000..5e4dd6c40c --- /dev/null +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -0,0 +1,220 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_EMPTY, + UNIT_VOLT_AMPS, + UNIT_WATT, + CONF_BUS_VOLTAGE, + CONF_BATTERY_VOLTAGE, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +# QPIRI sensors +CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" +CONF_GRID_RATING_CURRENT = "grid_rating_current" +CONF_AC_OUTPUT_RATING_VOLTAGE = "ac_output_rating_voltage" +CONF_AC_OUTPUT_RATING_FREQUENCY = "ac_output_rating_frequency" +CONF_AC_OUTPUT_RATING_CURRENT = "ac_output_rating_current" +CONF_AC_OUTPUT_RATING_APPARENT_POWER = "ac_output_rating_apparent_power" +CONF_AC_OUTPUT_RATING_ACTIVE_POWER = "ac_output_rating_active_power" +CONF_BATTERY_RATING_VOLTAGE = "battery_rating_voltage" +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_BULK_VOLTAGE = "battery_bulk_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_PARALLEL_MAX_NUM = "parallel_max_num" +CONF_MACHINE_TYPE = "machine_type" +CONF_TOPOLOGY = "topology" +CONF_OUTPUT_MODE = "output_mode" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +CONF_GRID_VOLTAGE = "grid_voltage" +CONF_GRID_FREQUENCY = "grid_frequency" +CONF_AC_OUTPUT_VOLTAGE = "ac_output_voltage" +CONF_AC_OUTPUT_FREQUENCY = "ac_output_frequency" +CONF_AC_OUTPUT_APPARENT_POWER = "ac_output_apparent_power" +CONF_AC_OUTPUT_ACTIVE_POWER = "ac_output_active_power" +CONF_OUTPUT_LOAD_PERCENT = "output_load_percent" +CONF_BATTERY_CHARGING_CURRENT = "battery_charging_current" +CONF_BATTERY_CAPACITY_PERCENT = "battery_capacity_percent" +CONF_INVERTER_HEAT_SINK_TEMPERATURE = "inverter_heat_sink_temperature" +CONF_PV_INPUT_CURRENT_FOR_BATTERY = "pv_input_current_for_battery" +CONF_PV_INPUT_VOLTAGE = "pv_input_voltage" +CONF_BATTERY_VOLTAGE_SCC = "battery_voltage_scc" +CONF_BATTERY_DISCHARGE_CURRENT = "battery_discharge_current" +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON = "battery_voltage_offset_for_fans_on" +CONF_EEPROM_VERSION = "eeprom_version" +CONF_PV_CHARGING_POWER = "pv_charging_power" + +TYPES = { + CONF_GRID_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_MACHINE_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_TOPOLOGY: sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY), + CONF_OUTPUT_MODE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_POWER_BALANCE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_GRID_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BUS_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + ), + CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_EEPROM_VERSION: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_CHARGING_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): schema for type, schema in TYPES.items()} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, _ in TYPES.items(): + if type in config: + conf = config[type] + sens = await sensor.new_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py new file mode 100644 index 0000000000..5ff33b10ff --- /dev/null +++ b/esphome/components/pipsolar/switch/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ICON_POWER, +) +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +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_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +TYPES = { + CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), + CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), + CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), + CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), + CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), +} + +PipsolarSwitch = pipsolar_ns.class_("PipsolarSwitch", switch.Switch, cg.Component) + +PIPSWITCH_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PipsolarSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Pipsolar switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon, + } +).extend(cv.COMPONENT_SCHEMA) + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): PIPSWITCH_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (on, off) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + cg.add(getattr(paren, f"set_{type}_switch")(var)) + cg.add(var.set_parent(paren)) + cg.add(var.set_on_command(on)) + if off is not None: + cg.add(var.set_off_command(off)) diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp new file mode 100644 index 0000000000..7eaeac1c2d --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp @@ -0,0 +1,24 @@ +#include "pipsolar_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.switch"; + +void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); } +void PipsolarSwitch::write_state(bool state) { + if (state) { + if (this->on_command_.length() > 0) { + this->parent_->switch_command(this->on_command_); + } + } else { + if (this->off_command_.length() > 0) { + this->parent_->switch_command(this->off_command_); + } + } +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.h b/esphome/components/pipsolar/switch/pipsolar_switch.h new file mode 100644 index 0000000000..11ff6c853a --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarSwitch : public switch_::Switch, public Component { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void set_on_command(const std::string &command) { this->on_command_ = command; }; + void set_off_command(const std::string &command) { this->off_command_ = command; }; + void dump_config() override; + + protected: + void write_state(bool state) override; + std::string on_command_; + std::string off_command_; + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/__init__.py b/esphome/components/pipsolar/text_sensor/__init__.py new file mode 100644 index 0000000000..fe6c4979f3 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/__init__.py @@ -0,0 +1,52 @@ +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_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CONF_DEVICE_MODE = "device_mode" +CONF_LAST_QPIGS = "last_qpigs" +CONF_LAST_QPIRI = "last_qpiri" +CONF_LAST_QMOD = "last_qmod" +CONF_LAST_QFLAG = "last_qflag" +CONF_LAST_QPIWS = "last_qpiws" +CONF_LAST_QT = "last_qt" +CONF_LAST_QMN = "last_qmn" + +PipsolarTextSensor = pipsolar_ns.class_( + "PipsolarTextSensor", text_sensor.TextSensor, cg.Component +) + +TYPES = [ + CONF_DEVICE_MODE, + CONF_LAST_QPIGS, + CONF_LAST_QPIRI, + CONF_LAST_QMOD, + CONF_LAST_QFLAG, + CONF_LAST_QPIWS, + CONF_LAST_QT, + CONF_LAST_QMN, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): text_sensor.TEXT_SENSOR_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PipsolarTextSensor)} + ) + for type in TYPES + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type in TYPES: + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + await cg.register_component(var, conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp new file mode 100644 index 0000000000..ee1fe2d1d8 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp @@ -0,0 +1,13 @@ +#include "pipsolar_textsensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.text_sensor"; + +void PipsolarTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pipsolar TextSensor", this); } + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h new file mode 100644 index 0000000000..871f6d8dee --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarTextSensor : public Component, public text_sensor::TextSensor { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void dump_config() override; + + protected: + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pm1006/__init__.py b/esphome/components/pm1006/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp new file mode 100644 index 0000000000..1a89307eee --- /dev/null +++ b/esphome/components/pm1006/pm1006.cpp @@ -0,0 +1,103 @@ +#include "pm1006.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pm1006 { + +static const char *const TAG = "pm1006"; + +static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B}; +static const uint8_t PM1006_REQUEST[] = {0x11, 0x02, 0x0B, 0x01, 0xE1}; + +void PM1006Component::setup() { + // because this implementation is currently rx-only, there is nothing to setup +} + +void PM1006Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM1006:"); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_UPDATE_INTERVAL(this); + this->check_uart_settings(9600); +} + +void PM1006Component::update() { + ESP_LOGV(TAG, "sending measurement request"); + this->write_array(PM1006_REQUEST, sizeof(PM1006_REQUEST)); +} + +void PM1006Component::loop() { + while (this->available() != 0) { + this->read_byte(&this->data_[this->data_index_]); + auto check = this->check_byte_(); + if (!check.has_value()) { + // finished + this->parse_data_(); + this->data_index_ = 0; + } else if (!*check) { + // wrong data + ESP_LOGV(TAG, "Byte %i of received data frame is invalid.", this->data_index_); + this->data_index_ = 0; + } else { + // next byte + this->data_index_++; + } + } +} + +float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } + +uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { + uint8_t sum = 0; + for (uint8_t i = 0; i < length; i++) { + sum += command_data[i]; + } + return sum; +} + +optional PM1006Component::check_byte_() const { + uint8_t index = this->data_index_; + uint8_t byte = this->data_[index]; + + // index 0..2 are the fixed header + if (index < sizeof(PM1006_RESPONSE_HEADER)) { + return byte == PM1006_RESPONSE_HEADER[index]; + } + + // just some additional notes here: + // index 3..4 is unused + // index 5..6 is our PM2.5 reading (3..6 is called DF1-DF4 in the datasheet at + // http://www.jdscompany.co.kr/download.asp?gubun=07&filename=PM1006_LED_PARTICLE_SENSOR_MODULE_SPECIFICATIONS.pdf + // that datasheet goes on up to DF16, which is unused for PM1006 but used in PM1006K + // so this code should be trivially extensible to support that one later + if (index < (sizeof(PM1006_RESPONSE_HEADER) + 16)) + return true; + + // checksum + if (index == (sizeof(PM1006_RESPONSE_HEADER) + 16)) { + uint8_t checksum = pm1006_checksum_(this->data_, sizeof(PM1006_RESPONSE_HEADER) + 17); + if (checksum != 0) { + ESP_LOGW(TAG, "PM1006 checksum is wrong: %02x, expected zero", checksum); + return false; + } + return {}; + } + + return false; +} + +void PM1006Component::parse_data_() { + const int pm_2_5_concentration = this->get_16_bit_uint_(5); + + ESP_LOGD(TAG, "Got PM2.5 Concentration: %d µg/m³", pm_2_5_concentration); + + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); + } +} + +uint16_t PM1006Component::get_16_bit_uint_(uint8_t start_index) const { + return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); +} + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h new file mode 100644 index 0000000000..238ac67006 --- /dev/null +++ b/esphome/components/pm1006/pm1006.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pm1006 { + +class PM1006Component : public PollingComponent, public uart::UARTDevice { + public: + PM1006Component() = default; + + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; } + void setup() override; + void dump_config() override; + void loop() override; + void update() override; + + float get_setup_priority() const override; + + protected: + optional check_byte_() const; + void parse_data_(); + uint16_t get_16_bit_uint_(uint8_t start_index) const; + uint8_t pm1006_checksum_(const uint8_t *command_data, uint8_t length) const; + + sensor::Sensor *pm_2_5_sensor_{nullptr}; + + uint8_t data_[20]; + uint8_t data_index_{0}; + uint32_t last_transmission_{0}; +}; + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py new file mode 100644 index 0000000000..1e648be199 --- /dev/null +++ b/esphome/components/pm1006/sensor.py @@ -0,0 +1,65 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_PM_2_5, + CONF_UPDATE_INTERVAL, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_BLUR, +) +from esphome.core import TimePeriodMilliseconds + +CODEOWNERS = ["@habbie"] +DEPENDENCIES = ["uart"] + +pm1006_ns = cg.esphome_ns.namespace("pm1006") +PM1006Component = pm1006_ns.class_("PM1006Component", uart.UARTDevice, cg.Component) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM1006Component), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_BLUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("never")), +) + + +def validate_interval_uart(config): + require_tx = False + + interval = config.get(CONF_UPDATE_INTERVAL) + + if isinstance(interval, TimePeriodMilliseconds): + # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects + require_tx = True + + uart.final_validate_device_schema( + "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + )(config) + + +FINAL_VALIDATE_SCHEMA = validate_interval_uart + + +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_PM_2_5 in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) diff --git a/esphome/components/pmsa003i/__init__.py b/esphome/components/pmsa003i/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp new file mode 100644 index 0000000000..ca3d28367a --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -0,0 +1,101 @@ +#include "pmsa003i.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace pmsa003i { + +static const char *const TAG = "pmsa003i"; + +void PMSA003IComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pmsa003i..."); + + PM25AQIData data; + bool successful_read = this->read_data_(&data); + + if (!successful_read) { + this->mark_failed(); + return; + } +} + +void PMSA003IComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void PMSA003IComponent::update() { + PM25AQIData data; + + bool successful_read = this->read_data_(&data); + + // Update sensors + if (successful_read) { + this->status_clear_warning(); + ESP_LOGV(TAG, "Read success. Updating sensors."); + + if (this->standard_units_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_standard); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_standard); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_standard); + } else { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_env); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_env); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_env); + } + + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(data.particles_03um); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(data.particles_05um); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(data.particles_10um); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(data.particles_25um); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(data.particles_50um); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(data.particles_100um); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Read failure. Skipping update."); + } +} + +bool PMSA003IComponent::read_data_(PM25AQIData *data) { + const uint8_t num_bytes = 32; + uint8_t buffer[num_bytes]; + + this->read_bytes_raw(buffer, num_bytes); + + // https://github.com/adafruit/Adafruit_PM25AQI + + // Check that start byte is correct! + if (buffer[0] != 0x42) { + return false; + } + + // get checksum ready + int16_t sum = 0; + for (uint8_t i = 0; i < 30; i++) { + sum += buffer[i]; + } + + // The data comes in endian'd, this solves it so it works on all platforms + uint16_t buffer_u16[15]; + for (uint8_t i = 0; i < 15; i++) { + buffer_u16[i] = buffer[2 + i * 2 + 1]; + buffer_u16[i] += (buffer[2 + i * 2] << 8); + } + + // put it into a nice struct :) + memcpy((void *) data, (void *) buffer_u16, 30); + + return (sum == data->checksum); +} + +} // namespace pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h new file mode 100644 index 0000000000..10176218ed --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pmsa003i { + +/**! Structure holding Plantower's standard packet **/ +// From https://github.com/adafruit/Adafruit_PM25AQI +struct PM25AQIData { + uint16_t framelen; ///< How long this data chunk is + uint16_t pm10_standard, ///< Standard PM1.0 + pm25_standard, ///< Standard PM2.5 + pm100_standard; ///< Standard PM10.0 + 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 unused; ///< Unused + uint16_t checksum; ///< Packet checksum +}; + +class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_standard_units(bool standard_units) { standard_units_ = standard_units; } + + 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: + bool read_data_(PM25AQIData *data); + + bool standard_units_; + + 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 pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py new file mode 100644 index 0000000000..ceca791cd6 --- /dev/null +++ b/esphome/components/pmsa003i/sensor.py @@ -0,0 +1,128 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, 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 = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +pmsa003i_ns = cg.esphome_ns.namespace("pmsa003i") + +PMSA003IComponent = pmsa003i_ns.class_( + "PMSA003IComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONF_STANDARD_UNITS = "standard_units" +UNIT_COUNTS_PER_100ML = "#/0.1L" +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PMSA003IComponent), + cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean, + 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_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x12)) +) + +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 i2c.register_i2c_device(var, config) + + cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS])) + + 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/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index abea287c0b..0474d6ffd0 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -6,9 +6,39 @@ namespace pmsx003 { static const char *const TAG = "pmsx003"; +void PMSX003Component::set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { + pm_1_0_std_sensor_ = pm_1_0_std_sensor; +} +void PMSX003Component::set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { + pm_2_5_std_sensor_ = pm_2_5_std_sensor; +} +void PMSX003Component::set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor) { + pm_10_0_std_sensor_ = pm_10_0_std_sensor; +} + void PMSX003Component::set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void PMSX003Component::set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void PMSX003Component::set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + +void PMSX003Component::set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor) { + pm_particles_03um_sensor_ = pm_particles_03um_sensor; +} +void PMSX003Component::set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor) { + pm_particles_05um_sensor_ = pm_particles_05um_sensor; +} +void PMSX003Component::set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor) { + pm_particles_10um_sensor_ = pm_particles_10um_sensor; +} +void PMSX003Component::set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor) { + pm_particles_25um_sensor_ = pm_particles_25um_sensor; +} +void PMSX003Component::set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor) { + pm_particles_50um_sensor_ = pm_particles_50um_sensor; +} +void PMSX003Component::set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor) { + pm_particles_100um_sensor_ = pm_particles_100um_sensor; +} + void PMSX003Component::set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } @@ -102,19 +132,68 @@ optional PMSX003Component::check_byte_() { void PMSX003Component::parse_data_() { switch (this->type_) { + case PMSX003_TYPE_5003ST: { + uint16_t formaldehyde = this->get_16_bit_uint_(28); + float temperature = 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%% Formaldehyde: %u µg/m^3", temperature, humidity, + formaldehyde); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + // The rest of the PMS5003ST matches the PMS5003, continue on + } case PMSX003_TYPE_X003: { + uint16_t pm_1_0_std_concentration = this->get_16_bit_uint_(4); + uint16_t pm_2_5_std_concentration = this->get_16_bit_uint_(6); + uint16_t pm_10_0_std_concentration = this->get_16_bit_uint_(8); + uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); + + uint16_t pm_particles_03um = this->get_16_bit_uint_(16); + uint16_t pm_particles_05um = this->get_16_bit_uint_(18); + uint16_t pm_particles_10um = this->get_16_bit_uint_(20); + uint16_t pm_particles_25um = this->get_16_bit_uint_(22); + uint16_t pm_particles_50um = this->get_16_bit_uint_(24); + uint16_t pm_particles_100um = this->get_16_bit_uint_(26); + ESP_LOGD(TAG, "Got PM1.0 Concentration: %u µg/m^3, PM2.5 Concentration %u µg/m^3, PM10.0 Concentration: %u µg/m^3", pm_1_0_concentration, pm_2_5_concentration, pm_10_0_concentration); + + if (this->pm_1_0_std_sensor_ != nullptr) + this->pm_1_0_std_sensor_->publish_state(pm_1_0_std_concentration); + if (this->pm_2_5_std_sensor_ != nullptr) + this->pm_2_5_std_sensor_->publish_state(pm_2_5_std_concentration); + if (this->pm_10_0_std_sensor_ != nullptr) + this->pm_10_0_std_sensor_->publish_state(pm_10_0_std_concentration); + if (this->pm_1_0_sensor_ != nullptr) this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); if (this->pm_2_5_sensor_ != nullptr) this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); if (this->pm_10_0_sensor_ != nullptr) this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); + + if (this->pm_particles_03um_sensor_ != nullptr) + this->pm_particles_03um_sensor_->publish_state(pm_particles_03um); + if (this->pm_particles_05um_sensor_ != nullptr) + this->pm_particles_05um_sensor_->publish_state(pm_particles_05um); + if (this->pm_particles_10um_sensor_ != nullptr) + this->pm_particles_10um_sensor_->publish_state(pm_particles_10um); + if (this->pm_particles_25um_sensor_ != nullptr) + this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); + if (this->pm_particles_50um_sensor_ != nullptr) + this->pm_particles_50um_sensor_->publish_state(pm_particles_50um); + if (this->pm_particles_100um_sensor_ != nullptr) + this->pm_particles_100um_sensor_->publish_state(pm_particles_100um); break; } case PMSX003_TYPE_5003T: { @@ -131,29 +210,6 @@ void PMSX003Component::parse_data_() { this->humidity_sensor_->publish_state(humidity); break; } - case PMSX003_TYPE_5003ST: { - uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); - uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); - uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); - uint16_t formaldehyde = this->get_16_bit_uint_(28); - float temperature = this->get_16_bit_uint_(30) / 10.0f; - float humidity = this->get_16_bit_uint_(32) / 10.0f; - ESP_LOGD(TAG, "Got PM2.5 Concentration: %u µg/m^3, Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", - pm_2_5_concentration, temperature, humidity, formaldehyde); - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->formaldehyde_sensor_ != nullptr) - this->formaldehyde_sensor_->publish_state(formaldehyde); - break; - } } this->status_clear_warning(); @@ -163,9 +219,21 @@ uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { } void PMSX003Component::dump_config() { ESP_LOGCONFIG(TAG, "PMSX003:"); + LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_); + LOG_SENSOR(" ", "PM2.5STD", this->pm_2_5_std_sensor_); + LOG_SENSOR(" ", "PM10.0STD", this->pm_10_0_std_sensor_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + + LOG_SENSOR(" ", "PM0.3um", this->pm_particles_03um_sensor_); + LOG_SENSOR(" ", "PM0.5um", this->pm_particles_05um_sensor_); + LOG_SENSOR(" ", "PM1.0um", this->pm_particles_10um_sensor_); + LOG_SENSOR(" ", "PM2.5um", this->pm_particles_25um_sensor_); + LOG_SENSOR(" ", "PM5.0um", this->pm_particles_50um_sensor_); + LOG_SENSOR(" ", "PM10.0um", this->pm_particles_100um_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index 163d25c694..a5adecb534 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -21,9 +21,22 @@ class PMSX003Component : public uart::UARTDevice, public Component { void dump_config() override; void set_type(PMSX003Type type) { type_ = type; } + + void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); + void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); + void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor); void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor); void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor); + + void set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor); + void set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor); + void set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor); + void set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor); + void set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor); + void set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor); + void set_temperature_sensor(sensor::Sensor *temperature_sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor); @@ -37,9 +50,25 @@ class PMSX003Component : public uart::UARTDevice, public Component { uint8_t data_index_{0}; uint32_t last_transmission_{0}; PMSX003Type type_; + + // "Standard Particle" + sensor::Sensor *pm_1_0_std_sensor_{nullptr}; + sensor::Sensor *pm_2_5_std_sensor_{nullptr}; + sensor::Sensor *pm_10_0_std_sensor_{nullptr}; + + // "Under Atmospheric Pressure" sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + + // Particle counts by size + sensor::Sensor *pm_particles_03um_sensor_{nullptr}; + sensor::Sensor *pm_particles_05um_sensor_{nullptr}; + sensor::Sensor *pm_particles_10um_sensor_{nullptr}; + sensor::Sensor *pm_particles_25um_sensor_{nullptr}; + sensor::Sensor *pm_particles_50um_sensor_{nullptr}; + sensor::Sensor *pm_particles_100um_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *formaldehyde_sensor_{nullptr}; diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 80f2b80e5e..350117a235 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -8,16 +8,28 @@ from esphome.const import ( CONF_PM_10_0, CONF_PM_1_0, CONF_PM_2_5, + CONF_PM_10_0_STD, + CONF_PM_1_0_STD, + CONF_PM_2_5_STD, + CONF_PM_0_3UM, + CONF_PM_0_5UM, + CONF_PM_1_0UM, + CONF_PM_2_5UM, + CONF_PM_5_0UM, + CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_CHEMICAL_WEAPON, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, + UNIT_COUNT_DECILITRE, UNIT_PERCENT, ) @@ -51,9 +63,7 @@ SENSORS_TO_TYPE = { def validate_pmsx003_sensors(value): for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: - raise cv.Invalid( - "{} does not have {} sensor!".format(value[CONF_TYPE], key) - ) + raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!") return value @@ -62,47 +72,95 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PMSX003Component), cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), - cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + cv.Optional(CONF_PM_1_0_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_PM1, + ), + cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_PM25, + ), + cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_PM10, + ), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, ICON_CHEMICAL_WEAPON, 0, DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -118,6 +176,18 @@ async def to_code(config): cg.add(var.set_type(config[CONF_TYPE])) + if CONF_PM_1_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0_STD]) + cg.add(var.set_pm_1_0_std_sensor(sens)) + + if CONF_PM_2_5_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5_STD]) + cg.add(var.set_pm_2_5_std_sensor(sens)) + + if CONF_PM_10_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0_STD]) + cg.add(var.set_pm_10_0_std_sensor(sens)) + if CONF_PM_1_0 in config: sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) @@ -130,6 +200,30 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_PM_0_3UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_3UM]) + cg.add(var.set_pm_particles_03um_sensor(sens)) + + if CONF_PM_0_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_5UM]) + cg.add(var.set_pm_particles_05um_sensor(sens)) + + if CONF_PM_1_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0UM]) + cg.add(var.set_pm_particles_10um_sensor(sens)) + + if CONF_PM_2_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5UM]) + cg.add(var.set_pm_particles_25um_sensor(sens)) + + if CONF_PM_5_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_5_0UM]) + cg.add(var.set_pm_particles_50um_sensor(sens)) + + if CONF_PM_10_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0UM]) + cg.add(var.set_pm_particles_100um_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/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index fc84f30078..ed2a2c1e35 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -1,5 +1,8 @@ #include "pn532.h" + +#include #include "esphome/core/log.h" +#include "esphome/core/hal.h" // Based on: // - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf @@ -49,7 +52,7 @@ void PN532::setup() { } // Set up SAM (secure access module) - uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); + uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, 0x01, // normal mode @@ -104,7 +107,7 @@ void PN532::loop() { if (!success) { // Something failed if (!this->current_uid_.empty()) { - auto tag = new nfc::NfcTag(this->current_uid_); + auto tag = make_unique(this->current_uid_); for (auto *trigger : this->triggers_ontagremoved_) trigger->process(tag); } @@ -117,7 +120,7 @@ void PN532::loop() { if (num_targets != 1) { // no tags found or too many if (!this->current_uid_.empty()) { - auto tag = new nfc::NfcTag(this->current_uid_); + auto tag = make_unique(this->current_uid_); for (auto *trigger : this->triggers_ontagremoved_) trigger->process(tag); } @@ -158,10 +161,10 @@ void PN532::loop() { if (report) { ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid(nfcid).c_str()); if (tag->has_ndef_message()) { - auto message = tag->get_ndef_message(); - auto records = message->get_records(); + const auto &message = tag->get_ndef_message(); + const auto &records = message->get_records(); ESP_LOGD(TAG, " NDEF formatted records:"); - for (auto &record : records) { + for (const auto &record : records) { ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); } } @@ -270,7 +273,7 @@ void PN532::turn_off_rf_() { }); } -nfc::NfcTag *PN532::read_tag_(std::vector &uid) { +std::unique_ptr PN532::read_tag_(std::vector &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { @@ -281,9 +284,9 @@ nfc::NfcTag *PN532::read_tag_(std::vector &uid) { return this->read_mifare_ultralight_tag_(uid); } else if (type == nfc::TAG_TYPE_UNKNOWN) { ESP_LOGV(TAG, "Cannot determine tag type"); - return new nfc::NfcTag(uid); + return make_unique(uid); } else { - return new nfc::NfcTag(uid); + return make_unique(uid); } } @@ -373,7 +376,9 @@ bool PN532BinarySensor::process(std::vector &data) { this->found_ = true; return true; } -void PN532OnTagTrigger::process(nfc::NfcTag *tag) { this->trigger(nfc::format_uid(tag->get_uid()), *tag); } +void PN532OnTagTrigger::process(const std::unique_ptr &tag) { + this->trigger(nfc::format_uid(tag->get_uid()), *tag); +} } // namespace pn532 } // namespace esphome diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index c4854cf7f2..692a5011e6 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -54,13 +54,13 @@ class PN532 : public PollingComponent { virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; - nfc::NfcTag *read_tag_(std::vector &uid); + std::unique_ptr read_tag_(std::vector &uid); bool format_tag_(std::vector &uid); bool clean_tag_(std::vector &uid); bool write_tag_(std::vector &uid, nfc::NdefMessage *message); - nfc::NfcTag *read_mifare_classic_tag_(std::vector &uid); + std::unique_ptr read_mifare_classic_tag_(std::vector &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); bool auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); @@ -68,7 +68,7 @@ class PN532 : public PollingComponent { bool format_mifare_classic_ndef_(std::vector &uid); bool write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message); - nfc::NfcTag *read_mifare_ultralight_tag_(std::vector &uid); + 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_(); uint16_t read_mifare_ultralight_capacity_(); @@ -117,7 +117,7 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { class PN532OnTagTrigger : public Trigger { public: - void process(nfc::NfcTag *tag); + void process(const std::unique_ptr &tag); }; class PN532OnFinishedWriteTrigger : public Trigger<> { diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index 119d1ceef6..81d135d8e6 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -1,3 +1,5 @@ +#include + #include "pn532.h" #include "esphome/core/log.h" @@ -6,7 +8,7 @@ namespace pn532 { static const char *const TAG = "pn532.mifare_classic"; -nfc::NfcTag *PN532::read_mifare_classic_tag_(std::vector &uid) { +std::unique_ptr PN532::read_mifare_classic_tag_(std::vector &uid) { uint8_t current_block = 4; uint8_t message_start_index = 0; uint32_t message_length = 0; @@ -15,15 +17,15 @@ nfc::NfcTag *PN532::read_mifare_classic_tag_(std::vector &uid) { std::vector data; if (this->read_mifare_classic_block_(current_block, data)) { if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { - return new nfc::NfcTag(uid, nfc::ERROR); + return make_unique(uid, nfc::ERROR); } } else { ESP_LOGE(TAG, "Failed to read block %d", current_block); - return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC); + return make_unique(uid, nfc::MIFARE_CLASSIC); } } else { ESP_LOGV(TAG, "Tag is not NDEF formatted"); - return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC); + return make_unique(uid, nfc::MIFARE_CLASSIC); } uint32_t index = 0; @@ -51,7 +53,7 @@ nfc::NfcTag *PN532::read_mifare_classic_tag_(std::vector &uid) { } } buffer.erase(buffer.begin(), buffer.begin() + message_start_index); - return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC, buffer); + return make_unique(uid, nfc::MIFARE_CLASSIC, buffer); } bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 1787a3c225..1b91ae919e 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -1,3 +1,5 @@ +#include + #include "pn532.h" #include "esphome/core/log.h" @@ -6,28 +8,28 @@ namespace pn532 { static const char *const TAG = "pn532.mifare_ultralight"; -nfc::NfcTag *PN532::read_mifare_ultralight_tag_(std::vector &uid) { +std::unique_ptr PN532::read_mifare_ultralight_tag_(std::vector &uid) { if (!this->is_mifare_ultralight_formatted_()) { ESP_LOGD(TAG, "Not NDEF formatted"); - return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + 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)) { - return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } ESP_LOGVV(TAG, "message length: %d, start: %d", message_length, message_start_index); if (message_length == 0) { - return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + 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); - return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + return make_unique(uid, nfc::NFC_FORUM_TYPE_2); } data.insert(data.end(), page_data.begin(), page_data.end()); @@ -38,7 +40,7 @@ nfc::NfcTag *PN532::read_mifare_ultralight_tag_(std::vector &uid) { data.erase(data.begin(), data.begin() + message_start_index); data.erase(data.begin() + message_length, data.end()); - return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2, data); + return make_unique(uid, nfc::NFC_FORUM_TYPE_2, data); } bool PN532::read_mifare_ultralight_page_(uint8_t page_num, std::vector &data) { diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index 25f24758bf..e7c99e94b0 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -1,5 +1,6 @@ #include "pn532_i2c.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" // Based on: // - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf @@ -11,7 +12,9 @@ namespace pn532_i2c { static const char *const TAG = "pn532_i2c"; -bool PN532I2C::write_data(const std::vector &data) { return this->write_bytes_raw(data.data(), data.size()); } +bool PN532I2C::write_data(const std::vector &data) { + return this->write(data.data(), data.size()) == i2c::ERROR_OK; +} bool PN532I2C::read_data(std::vector &data, uint8_t len) { delay(1); diff --git a/esphome/components/pn532_spi/__init__.py b/esphome/components/pn532_spi/__init__.py index 2683f34ad5..8a8ab1b175 100644 --- a/esphome/components/pn532_spi/__init__.py +++ b/esphome/components/pn532_spi/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["spi"] +MULTI_CONF = True pn532_spi_ns = cg.esphome_ns.namespace("pn532_spi") PN532Spi = pn532_spi_ns.class_("PN532Spi", pn532.PN532, spi.SPIDevice) diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index f50adac6f9..a492919202 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -42,7 +42,7 @@ void PowerSupply::request_high_power() { void PowerSupply::unrequest_high_power() { this->active_requests_--; if (this->active_requests_ < 0) { - // we're just going to use 0 as our now counter. + // we're just going to use 0 as our new counter. this->active_requests_ = 0; } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 12d2a9984f..66e4a7565a 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace power_supply { diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py new file mode 100644 index 0000000000..4844ad6c02 --- /dev/null +++ b/esphome/components/preferences/__init__.py @@ -0,0 +1,24 @@ +from esphome.const import CONF_ID +import esphome.codegen as cg +import esphome.config_validation as cv + +CODEOWNERS = ["@esphome/core"] + +preferences_ns = cg.esphome_ns.namespace("preferences") +IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.Component) + +CONF_FLASH_WRITE_INTERVAL = "flash_write_interval" +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(IntervalSyncer), + cv.Optional( + CONF_FLASH_WRITE_INTERVAL, default="60s" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_write_interval(config[CONF_FLASH_WRITE_INTERVAL])) + await cg.register_component(var, config) diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h new file mode 100644 index 0000000000..af1fe9ba4a --- /dev/null +++ b/esphome/components/preferences/syncer.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/preferences.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace preferences { + +class IntervalSyncer : public Component { + public: + void set_write_interval(uint32_t write_interval) { write_interval_ = write_interval; } + void setup() override { + set_interval(write_interval_, []() { global_preferences->sync(); }); + } + void on_shutdown() override { global_preferences->sync(); } + float get_setup_priority() const override { return setup_priority::BUS; } + + protected: + uint32_t write_interval_; +}; + +} // namespace preferences +} // namespace esphome diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 06a0e39e2c..fa7b4fe132 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ARDUINO + #include "prometheus_handler.h" #include "esphome/core/application.h" @@ -55,7 +57,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { if (obj->is_internal()) return; - if (!isnan(obj->state)) { + if (!std::isnan(obj->state)) { // We have a valid value, output this value stream->print(F("esphome_sensor_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -249,7 +251,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { if (obj->is_internal()) return; - if (!isnan(obj->position)) { + if (!std::isnan(obj->position)) { // We have a valid value, output this value stream->print(F("esphome_cover_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -310,3 +312,5 @@ void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch } // namespace prometheus } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 6abd406556..5076883ba6 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/controller.h" #include "esphome/core/component.h" @@ -79,3 +81,5 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } // namespace prometheus } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 9a0cf568a9..f538a4c905 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -8,15 +8,15 @@ static const char *const TAG = "pulse_counter"; const char *const EDGE_MODE_TO_STRING[] = {"DISABLE", "INCREMENT", "DECREMENT"}; -#ifdef ARDUINO_ARCH_ESP8266 -void ICACHE_RAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) { +#ifdef USE_ESP8266 +void IRAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) { const uint32_t now = micros(); const bool discard = now - arg->last_pulse < arg->filter_us; arg->last_pulse = now; if (discard) return; - PulseCounterCountMode mode = arg->isr_pin->digital_read() ? arg->rising_edge_mode : arg->falling_edge_mode; + PulseCounterCountMode mode = arg->isr_pin.digital_read() ? arg->rising_edge_mode : arg->falling_edge_mode; switch (mode) { case PULSE_COUNTER_DISABLE: break; @@ -28,11 +28,11 @@ void ICACHE_RAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) { break; } } -bool PulseCounterStorage::pulse_counter_setup(GPIOPin *pin) { +bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { this->pin = pin; this->pin->setup(); this->isr_pin = this->pin->to_isr(); - this->pin->attach_interrupt(PulseCounterStorage::gpio_intr, this, CHANGE); + this->pin->attach_interrupt(PulseCounterStorage::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); return true; } pulse_counter_t PulseCounterStorage::read_raw_value() { @@ -43,12 +43,13 @@ pulse_counter_t PulseCounterStorage::read_raw_value() { } #endif -#ifdef ARDUINO_ARCH_ESP32 -bool PulseCounterStorage::pulse_counter_setup(GPIOPin *pin) { +#ifdef USE_ESP32 +bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { + static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; this->pin = pin; this->pin->setup(); this->pcnt_unit = next_pcnt_unit; - next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); // NOLINT + next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); ESP_LOGCONFIG(TAG, " PCNT Unit Number: %u", this->pcnt_unit); @@ -166,9 +167,5 @@ void PulseCounterSensor::update() { } } -#ifdef ARDUINO_ARCH_ESP32 -pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; -#endif - } // namespace pulse_counter } // namespace esphome diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index b3e3f42c01..94e37bc232 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -1,10 +1,10 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif @@ -17,30 +17,30 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 using pulse_counter_t = int16_t; #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 using pulse_counter_t = int32_t; #endif struct PulseCounterStorage { - bool pulse_counter_setup(GPIOPin *pin); + bool pulse_counter_setup(InternalGPIOPin *pin); pulse_counter_t read_raw_value(); static void gpio_intr(PulseCounterStorage *arg); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 volatile pulse_counter_t counter{0}; volatile uint32_t last_pulse{0}; #endif - GPIOPin *pin; -#ifdef ARDUINO_ARCH_ESP32 + InternalGPIOPin *pin; +#ifdef USE_ESP32 pcnt_unit_t pcnt_unit; #endif -#ifdef ARDUINO_ARCH_ESP8266 - ISRInternalGPIOPin *isr_pin; +#ifdef USE_ESP8266 + ISRInternalGPIOPin isr_pin; #endif PulseCounterCountMode rising_edge_mode{PULSE_COUNTER_INCREMENT}; PulseCounterCountMode falling_edge_mode{PULSE_COUNTER_DISABLE}; @@ -50,7 +50,7 @@ struct PulseCounterStorage { class PulseCounterSensor : public sensor::Sensor, public PollingComponent { public: - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_rising_edge_mode(PulseCounterCountMode mode) { storage_.rising_edge_mode = mode; } void set_falling_edge_mode(PulseCounterCountMode mode) { storage_.falling_edge_mode = mode; } void set_filter_us(uint32_t filter) { storage_.filter_us = filter; } @@ -63,15 +63,11 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; PulseCounterStorage storage_; uint32_t current_total_ = 0; sensor::Sensor *total_sensor_; }; -#ifdef ARDUINO_ARCH_ESP32 -extern pcnt_unit_t next_pcnt_unit; -#endif - } // namespace pulse_counter } // namespace esphome diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 71227ec491..c7b89d41b0 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -11,10 +11,9 @@ from esphome.const import ( CONF_RISING_EDGE, CONF_NUMBER, CONF_TOTAL, - DEVICE_CLASS_EMPTY, ICON_PULSE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES_PER_MINUTE, UNIT_PULSES, ) @@ -67,11 +66,10 @@ def validate_count_mode(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, - ICON_PULSE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -94,7 +92,10 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 1a35deba2f..fd1403b4fd 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "pulse_meter"; void PulseMeterSensor::setup() { this->pin_->setup(); this->isr_pin_ = pin_->to_isr(); - this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, CHANGE); + this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); this->last_detected_edge_us_ = 0; this->last_valid_edge_us_ = 0; @@ -56,14 +56,14 @@ void PulseMeterSensor::dump_config() { ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %us", this->timeout_us_ / 1000000); } -void ICACHE_RAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { +void IRAM_ATTR PulseMeterSensor::gpio_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(); // We only look at rising edges - if (!sensor->isr_pin_->digital_read()) { + if (!sensor->isr_pin_.digital_read()) { return; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index d02cff6123..1cebc1748e 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/helpers.h" @@ -10,7 +10,7 @@ namespace pulse_meter { class PulseMeterSensor : public sensor::Sensor, public Component { public: - void set_pin(GPIOPin *pin) { this->pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_filter_us(uint32_t filter) { this->filter_us_ = filter; } void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; } void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; } @@ -25,8 +25,8 @@ class PulseMeterSensor : public sensor::Sensor, public Component { protected: static void gpio_intr(PulseMeterSensor *sensor); - GPIOPin *pin_ = nullptr; - ISRInternalGPIOPin *isr_pin_; + InternalGPIOPin *pin_ = nullptr; + ISRInternalGPIOPin isr_pin_; uint32_t filter_us_ = 0; uint32_t timeout_us_ = 1000000UL * 60UL * 5UL; sensor::Sensor *total_sensor_ = nullptr; diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index e732971c3a..454cb3a69d 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -12,10 +12,9 @@ from esphome.const import ( CONF_VALUE, ICON_PULSE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, - DEVICE_CLASS_EMPTY, ) from esphome.core import CORE @@ -51,7 +50,10 @@ def validate_pulse_meter_pin(value): CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(PulseMeterSensor), @@ -59,7 +61,10 @@ CONFIG_SCHEMA = sensor.sensor_schema( cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index fb998ef4e1..8d66861049 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -6,8 +6,8 @@ namespace pulse_width { static const char *const TAG = "pulse_width"; -void ICACHE_RAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { - const bool new_level = arg->pin_->digital_read(); +void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { + const bool new_level = arg->pin_.digital_read(); const uint32_t now = micros(); if (new_level) { arg->last_rise_ = now; diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index 9d32ce99b1..822688ec88 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -10,11 +10,11 @@ namespace pulse_width { /// Store data in a class that doesn't use multiple-inheritance (vtables in flash) class PulseWidthSensorStore { public: - void setup(GPIOPin *pin) { + void setup(InternalGPIOPin *pin) { pin->setup(); this->pin_ = pin->to_isr(); this->last_rise_ = micros(); - pin->attach_interrupt(&PulseWidthSensorStore::gpio_intr, this, CHANGE); + pin->attach_interrupt(&PulseWidthSensorStore::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); } static void gpio_intr(PulseWidthSensorStore *arg); uint32_t get_pulse_width_us() const { return this->last_width_; } @@ -22,14 +22,14 @@ class PulseWidthSensorStore { uint32_t get_last_rise() const { return last_rise_; } protected: - ISRInternalGPIOPin *pin_; + ISRInternalGPIOPin pin_; volatile uint32_t last_width_{0}; volatile uint32_t last_rise_{0}; }; class PulseWidthSensor : public sensor::Sensor, public PollingComponent { public: - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override { this->store_.setup(this->pin_); } void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -37,7 +37,7 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { protected: PulseWidthSensorStore store_; - GPIOPin *pin_; + InternalGPIOPin *pin_; }; } // namespace pulse_width diff --git a/esphome/components/pulse_width/sensor.py b/esphome/components/pulse_width/sensor.py index 6a6147c6aa..b090647627 100644 --- a/esphome/components/pulse_width/sensor.py +++ b/esphome/components/pulse_width/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_SECOND, ICON_TIMER, @@ -19,14 +18,15 @@ PulseWidthSensor = pulse_width_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { cv.GenerateID(): cv.declare_id(PulseWidthSensor), - cv.Required(CONF_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } ) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/pvvx_mithermometer/__init__.py b/esphome/components/pvvx_mithermometer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp new file mode 100644 index 0000000000..a41ad1bfcb --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -0,0 +1,139 @@ +#include "pvvx_mithermometer.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace pvvx_mithermometer { + +static const char *const TAG = "pvvx_mithermometer"; + +void PVVXMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "PVVX MiThermometer"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); +} + +bool PVVXMiThermometer::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 = parse_header_(service_data); + if (!res.has_value()) { + continue; + } + if (!(parse_message_(service_data.data, *res))) { + continue; + } + if (!(report_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); + if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) + this->battery_voltage_->publish_state(*res->battery_voltage); + success = true; + } + + return success; +} + +optional PVVXMiThermometer::parse_header_(const esp32_ble_tracker::ServiceData &service_data) { + ParseResult result; + if (!service_data.uuid.contains(0x1A, 0x18)) { + ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + return {}; + } + last_frame_count = raw[13]; + + return result; +} + +bool PVVXMiThermometer::parse_message_(const std::vector &message, ParseResult &result) { + /* + All data little endian + uint8_t size; // = 19 + uint8_t uid; // = 0x16, 16-bit UUID + uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - lo, .. [5] - hi digits + int16_t temperature; // x 0.01 degree [6,7] + uint16_t humidity; // x 0.01 % [8,9] + uint16_t battery_mv; // mV [10,11] + uint8_t battery_level; // 0..100 % [12] + uint8_t counter; // measurement count [13] + uint8_t flags; [14] + */ + + const uint8_t *data = message.data(); + const int data_length = 15; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + // int16_t temperature; // x 0.01 degree [6,7] + const int16_t temperature = int16_t(data[6]) | (int16_t(data[7]) << 8); + result.temperature = temperature / 1.0e2f; + + // uint16_t humidity; // x 0.01 % [8,9] + const int16_t humidity = uint16_t(data[8]) | (uint16_t(data[9]) << 8); + result.humidity = humidity / 1.0e2f; + + // uint16_t battery_mv; // mV [10,11] + const int16_t battery_voltage = uint16_t(data[10]) | (uint16_t(data[11]) << 8); + result.battery_voltage = battery_voltage / 1.0e3f; + + // uint8_t battery_level; // 0..100 % [12] + result.battery_level = uint8_t(data[12]); + + return true; +} + +bool PVVXMiThermometer::report_results_(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got PVVX MiThermometer (%s):", address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.2f °C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.2f %%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f %%", *result->battery_level); + } + if (result->battery_voltage.has_value()) { + ESP_LOGD(TAG, " Battery Voltage: %.3f V", *result->battery_voltage); + } + + return true; +} + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h new file mode 100644 index 0000000000..ad8baed35f --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -0,0 +1,47 @@ +#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 pvvx_mithermometer { + +struct ParseResult { + optional temperature; + optional humidity; + optional battery_level; + optional battery_voltage; + int raw_offset; +}; + +class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { 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) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message_(const std::vector &message, ParseResult &result); + bool report_results_(const optional &result, const std::string &address); +}; + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py new file mode 100644 index 0000000000..b17878f01b --- /dev/null +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -0,0 +1,84 @@ +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_BATTERY_VOLTAGE, + CONF_MAC_ADDRESS, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_ID, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@pasiz"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +pvvx_mithermometer_ns = cg.esphome_ns.namespace("pvvx_mithermometer") +PVVXMiThermometer = pvvx_mithermometer_ns.class_( + "PVVXMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PVVXMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + 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, + ), + } + ) + .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 CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_BATTERY_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + cg.add(var.set_battery_voltage(sens)) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 969ce8fa10..e5418765bd 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -6,6 +6,14 @@ namespace pzem004t { static const char *const TAG = "pzem004t"; +void PZEM004T::setup() { + // Clear UART buffer + while (this->available()) + this->read(); + // Set module address + this->write_state_(SET_ADDRESS); +} + void PZEM004T::loop() { const uint32_t now = millis(); if (now - this->last_read_ > 500 && this->available() < 7) { diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index 517b81eb21..f4f9f29b4d 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -14,6 +14,8 @@ class PZEM004T : 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 setup() override; + void loop() override; void update() override; diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index e3859f090c..70dec82c3f 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -11,9 +11,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -30,24 +29,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEM004T), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + 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_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 778c5054a0..b6697e3d19 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -9,21 +9,18 @@ from esphome.const import ( CONF_VOLTAGE, CONF_FREQUENCY, CONF_POWER_FACTOR, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY, - ICON_EMPTY, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_WATT_HOURS, ) @@ -37,38 +34,39 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMAC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 58afea8e30..08ec688afb 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -26,17 +25,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMDC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + 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_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 43e7939cf1..f03b6af191 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -1,5 +1,7 @@ #include "qmc5883l.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include namespace esphome { namespace qmc5883l { @@ -115,9 +117,10 @@ void QMC5883LComponent::update() { } bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { - bool success = this->read_byte_16(a_register, data); - *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte oder, LSB first; - return success; + if (!this->read_byte_16(a_register, data)) + return false; + *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte order, LSB first; + return true; } } // namespace qmc5883l diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index d0fdf1b77a..27d1df5b29 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -71,10 +70,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 789ab6197b..385641fea0 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -43,14 +43,14 @@ void RC522::setup() { // First set the resetPowerDownPin as digital input, to check the MFRC522 power down mode. if (reset_pin_ != nullptr) { - reset_pin_->pin_mode(INPUT); + reset_pin_->pin_mode(gpio::FLAG_INPUT); - if (reset_pin_->digital_read() == LOW) { // The MFRC522 chip is in power down mode. + if (!reset_pin_->digital_read()) { // The MFRC522 chip is in power down mode. ESP_LOGV(TAG, "Power down mode detected. Hard resetting..."); - reset_pin_->pin_mode(OUTPUT); // Now set the resetPowerDownPin as digital output. - reset_pin_->digital_write(LOW); // Make sure we have a clean LOW state. + reset_pin_->pin_mode(gpio::FLAG_OUTPUT); // Now set the resetPowerDownPin as digital output. + reset_pin_->digital_write(false); // Make sure we have a clean LOW state. delayMicroseconds(2); // 8.8.1 Reset timing requirements says about 100ns. Let us be generous: 2μsl - reset_pin_->digital_write(HIGH); // Exit power down mode. This triggers a hard reset. + reset_pin_->digital_write(true); // Exit power down mode. This triggers a hard reset. // Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. // Let us be generous: 50ms. reset_timeout_ = millis(); @@ -162,7 +162,7 @@ void RC522::loop() { ESP_LOGW(TAG, "CMD_REQA -> Not OK %d", status); state_ = STATE_DONE; } else if (back_length_ != 2) { // || *valid_bits_ != 0) { // ATQA must be exactly 16 bits. - ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpacted back_length_ of %d", back_length_); + ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpected back_length_ of %d", back_length_); state_ = STATE_DONE; } else { state_ = STATE_READ_SERIAL; @@ -470,7 +470,7 @@ RC522::StatusCode RC522::await_crc_() { return STATUS_WAITING; ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT"); - // 89ms passed and nothing happend. Communication with the MFRC522 might be down. + // 89ms passed and nothing happened. Communication with the MFRC522 might be down. return STATUS_TIMEOUT; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index 7fb49e97fd..d853d2f5ff 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/binary_sensor/binary_sensor.h" @@ -32,7 +33,7 @@ class RC522 : public PollingComponent { STATUS_OK, // Success STATUS_WAITING, // Waiting result from RC522 chip STATUS_ERROR, // Error in communication - STATUS_COLLISION, // Collission detected + STATUS_COLLISION, // Collision detected STATUS_TIMEOUT, // Timeout in communication. STATUS_NO_ROOM, // A buffer is not big enough. STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-) diff --git a/esphome/components/rc522_i2c/rc522_i2c.cpp b/esphome/components/rc522_i2c/rc522_i2c.cpp index 896e27214a..6a3d8d2486 100644 --- a/esphome/components/rc522_i2c/rc522_i2c.cpp +++ b/esphome/components/rc522_i2c/rc522_i2c.cpp @@ -18,8 +18,9 @@ void RC522I2C::dump_config() { uint8_t RC522I2C::pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. ) { uint8_t value; - read_byte(reg >> 1, &value); - ESP_LOGVV(TAG, "read_register_(%x) -> %x", reg, value); + if (!read_byte(reg >> 1, &value)) + return 0; + ESP_LOGVV(TAG, "read_register_(%x) -> %u", reg, value); return value; } diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py index 68b1e64145..77b0a99662 100644 --- a/esphome/components/rc522_spi/__init__.py +++ b/esphome/components/rc522_spi/__init__.py @@ -19,13 +19,12 @@ CONFIG_SCHEMA = cv.All( ).extend(spi.spi_device_schema(cs_pin_required=True)) ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "rc522_spi", require_miso=True, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await rc522.setup_rc522(var, config) await spi.register_spi_device(var, config) - - -def validate(config, item_config): - # validate given SPI hub is suitable for rc522_spi, it needs both miso and mosi - spi.validate_device("rc522_spi", config, item_config, True, True) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 76e4b51bde..d2b848600d 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -176,7 +176,9 @@ validate_binary_sensor = cv.validate_registry_entry( TRIGGER_REGISTRY = SimpleRegistry() DUMPER_REGISTRY = Registry( { - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + cv.Optional(CONF_RECEIVER_ID): cv.invalid( + "This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver." + ), } ) @@ -228,12 +230,53 @@ async def build_dumpers(config): dumpers = [] for conf in config: dumper = await cg.build_registry_entry(DUMPER_REGISTRY, conf) - receiver = await cg.get_variable(conf[CONF_RECEIVER_ID]) - cg.add(receiver.register_dumper(dumper)) dumpers.append(dumper) return dumpers +# Dish +DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( + "Dish" +) +DISH_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ADDRESS, default=1): cv.int_range(min=1, max=16), + cv.Required(CONF_COMMAND): cv.int_range(min=0, max=63), + } +) + + +@register_binary_sensor("dish", DishBinarySensor, DISH_SCHEMA) +def dish_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DishData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("dish", DishTrigger, DishData) +def dish_trigger(var, config): + pass + + +@register_dumper("dish", DishDumper) +def dish_dumper(var, config): + pass + + +@register_action("dish", DishAction, DISH_SCHEMA) +async def dish_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_)) + + # JVC JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) @@ -448,8 +491,7 @@ def validate_raw_alternating(value): if i != 0: if this_negative == last_negative: raise cv.Invalid( - "Values must alternate between being positive and negative, " - "please see index {} and {}".format(i, i + 1), + f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", [i], ) last_negative = this_negative @@ -576,13 +618,11 @@ def validate_rc_switch_code(value): for c in value: if c not in ("0", "1"): raise cv.Invalid( - "Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" - "".format(c) + f"Invalid RCSwitch code character '{c}'. Only '0' and '1' are allowed" ) if len(value) > 64: raise cv.Invalid( - "Maximum length for RCSwitch codes is 64, code '{}' has length {}" - "".format(value, len(value)) + f"Maximum length for RCSwitch codes is 64, code '{value}' has length {len(value)}" ) if not value: raise cv.Invalid("RCSwitch code must not be empty") @@ -595,14 +635,11 @@ def validate_rc_switch_raw_code(value): for c in value: if c not in ("0", "1", "x"): raise cv.Invalid( - "Invalid RCSwitch raw code character '{}'.Only '0', '1' and 'x' are allowed".format( - c - ) + f"Invalid RCSwitch raw code character '{c}'.Only '0', '1' and 'x' are allowed" ) if len(value) > 64: raise cv.Invalid( - "Maximum length for RCSwitch raw codes is 64, code '{}' has length {}" - "".format(value, len(value)) + f"Maximum length for RCSwitch raw codes is 64, code '{value}' has length {len(value)}" ) if not value: raise cv.Invalid("RCSwitch raw code must not be empty") @@ -866,7 +903,8 @@ def rc_switch_dumper(var, config): ) = declare_protocol("Samsung") SAMSUNG_SCHEMA = cv.Schema( { - cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Required(CONF_DATA): cv.hex_uint64_t, + cv.Optional(CONF_NBITS, default=32): cv.int_range(32, 64), } ) @@ -878,6 +916,7 @@ def samsung_binary_sensor(var, config): cg.StructInitializer( SamsungData, ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), ) ) ) @@ -895,8 +934,10 @@ def samsung_dumper(var, config): @register_action("samsung", SamsungAction, SAMSUNG_SCHEMA) async def samsung_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint64) cg.add(var.set_data(template_)) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) + cg.add(var.set_nbits(template_)) # Samsung36 @@ -946,6 +987,53 @@ async def samsung36_action(var, config, args): cg.add(var.set_command(template_)) +# Toshiba AC +( + ToshibaAcData, + ToshibaAcBinarySensor, + ToshibaAcTrigger, + ToshibaAcAction, + ToshibaAcDumper, +) = declare_protocol("ToshibaAc") +TOSHIBAAC_SCHEMA = cv.Schema( + { + cv.Required(CONF_RC_CODE_1): cv.hex_uint64_t, + cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint64_t, + } +) + + +@register_binary_sensor("toshiba_ac", ToshibaAcBinarySensor, TOSHIBAAC_SCHEMA) +def toshibaac_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + ToshibaAcData, + ("rc_code_1", config[CONF_RC_CODE_1]), + ("rc_code_2", config[CONF_RC_CODE_2]), + ) + ) + ) + + +@register_trigger("toshiba_ac", ToshibaAcTrigger, ToshibaAcData) +def toshibaac_trigger(var, config): + pass + + +@register_dumper("toshiba_ac", ToshibaAcDumper) +def toshibaac_dumper(var, config): + pass + + +@register_action("toshiba_ac", ToshibaAcAction, TOSHIBAAC_SCHEMA) +async def toshibaac_action(var, config, args): + template_ = await cg.templatable(config[CONF_RC_CODE_1], args, cg.uint64) + cg.add(var.set_rc_code_1(template_)) + template_ = await cg.templatable(config[CONF_RC_CODE_2], args, cg.uint64) + cg.add(var.set_rc_code_2(template_)) + + # Panasonic ( PanasonicData, @@ -991,3 +1079,42 @@ async def panasonic_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32) cg.add(var.set_command(template_)) + + +# Midea +MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( + "Midea" +) +MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) +MIDEA_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t, cv.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])) + + +@register_trigger("midea", MideaTrigger, MideaData) +def midea_trigger(var, config): + pass + + +@register_dumper("midea", MideaDumper) +def midea_dumper(var, config): + pass + + +@register_action( + "midea", + MideaAction, + MIDEA_SCHEMA, +) +async def midea_action(var, config, args): + cg.add(var.set_code(config[CONF_CODE])) diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp new file mode 100644 index 0000000000..1257e22a45 --- /dev/null +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -0,0 +1,92 @@ +#include "dish_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.dish"; + +static const uint32_t HEADER_HIGH_US = 400; +static const uint32_t HEADER_LOW_US = 6100; +static const uint32_t BIT_HIGH_US = 400; +static const uint32_t BIT_ONE_LOW_US = 1700; +static const uint32_t BIT_ZERO_LOW_US = 2800; + +void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { + dst->reserve(138); + dst->set_carrier_frequency(57600); + + // HEADER + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + + // Typically a DISH device needs to get a command a total of + // at least 4 times to accept it. + for (uint i = 0; i < 4; i++) { + // COMMAND (function, in MSB) + for (uint8_t mask = 1UL << 5; 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); + } + + // ADDRESS (unit code, in LSB) + for (uint8_t mask = 1UL; mask < 1UL << 4; mask <<= 1) { + if ((data.address - 1) & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + // PADDING + for (uint j = 0; j < 6; j++) + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + + // FOOTER + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + } +} +optional DishProtocol::decode(RemoteReceiveData src) { + DishData data{ + .address = 0, + .command = 0, + }; + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint8_t mask = 1UL << 5; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + data.command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + data.command &= ~mask; + } else { + return {}; + } + } + + for (uint8_t mask = 1UL; mask < 1UL << 5; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + data.address |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + data.address &= ~mask; + } else { + return {}; + } + } + for (uint j = 0; j < 6; j++) { + if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + return {}; + } + } + data.address++; + + src.expect_item(HEADER_HIGH_US, HEADER_LOW_US); + + return data; +} + +void DishProtocol::dump(const DishData &data) { + ESP_LOGD(TAG, "Received Dish: address=0x%02X, command=0x%02X", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/dish_protocol.h b/esphome/components/remote_base/dish_protocol.h new file mode 100644 index 0000000000..ca4d04ed34 --- /dev/null +++ b/esphome/components/remote_base/dish_protocol.h @@ -0,0 +1,38 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct DishData { + uint8_t address; + uint8_t command; + + bool operator==(const DishData &rhs) const { return address == rhs.address && command == rhs.command; } +}; + +class DishProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DishData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DishData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Dish) + +template class DishAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DishData data{}; + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + DishProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp new file mode 100644 index 0000000000..baf64f246f --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -0,0 +1,99 @@ +#include "midea_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.midea"; + +uint8_t MideaData::calc_cs_() const { + uint8_t cs = 0; + for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) + cs -= reverse_bits_8(*it); + return reverse_bits_8(cs); +} + +bool MideaData::check_compliment(const MideaData &rhs) const { + const uint8_t *it0 = rhs.data(); + for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { + if (*it0 != ~(*it1)) + return false; + } + return true; +} + +void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { + for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { + const uint8_t data = compliment ? ~(*it) : *it; + for (uint8_t mask = 128; mask; mask >>= 1) { + if (data & mask) + one(dst); + else + zero(dst); + } + } +} + +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data); + MideaProtocol::footer(dst); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data, true); + MideaProtocol::footer(dst); +} + +bool MideaProtocol::expect_one(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_zero(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_header(RemoteReceiveData &src) { + if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_footer(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { + for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { + for (uint8_t mask = 128; mask; mask >>= 1) { + if (MideaProtocol::expect_one(src)) + *dst |= mask; + else if (!MideaProtocol::expect_zero(src)) + return false; + } + } + return true; +} + +optional MideaProtocol::decode(RemoteReceiveData src) { + MideaData out, inv; + if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && + out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + return out; + return {}; +} + +void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(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 new file mode 100644 index 0000000000..35ea23acfb --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.h @@ -0,0 +1,104 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +class MideaData { + public: + // Make zero-filled + MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make from initializer_list + MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + // Make from vector + MideaData(const std::vector &data) { + memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + } + // Default copy constructor + MideaData(const MideaData &) = default; + + uint8_t *data() { return this->data_; } + const uint8_t *data() const { return this->data_; } + uint8_t size() const { return sizeof(this->data_); } + bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } + void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } + bool check_compliment(const MideaData &rhs) const; + std::string to_string() const { return hexencode(*this); } + // compare only 40-bits + bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + enum MideaDataType : uint8_t { + MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_SPECIAL = 0xA2, + MIDEA_TYPE_FOLLOW_ME = 0xA4, + }; + MideaDataType type() const { return static_cast(this->data_[0]); } + template T to() const { return T(*this); } + + protected: + void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { + data_[offset] &= ~(val_mask << shift); + data_[offset] |= (val << shift); + } + static const uint8_t OFFSET_CS = 5; + // 48-bits data + uint8_t data_[6]; + // Calculate checksum + uint8_t calc_cs_() const; +}; + +class MideaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MideaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MideaData &data) override; + + protected: + static const int32_t TICK_US = 560; + static const int32_t HEADER_HIGH_US = 8 * TICK_US; + static const int32_t HEADER_LOW_US = 8 * TICK_US; + static const int32_t BIT_HIGH_US = 1 * TICK_US; + static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; + static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; + static const int32_t MIN_GAP_US = 10 * TICK_US; + static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } + static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } + static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } + static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } + static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); + static bool expect_one(RemoteReceiveData &src); + static bool expect_zero(RemoteReceiveData &src); + static bool expect_header(RemoteReceiveData &src); + static bool expect_footer(RemoteReceiveData &src); + static bool expect_data(RemoteReceiveData &src, MideaData &out); +}; + +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; + +template class MideaAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(std::vector, code) + void set_code(const std::vector &code) { code_ = code; } + void encode(RemoteTransmitData *dst, Ts... x) override { + MideaData data = this->code_.value(x...); + data.finalize(); + MideaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 80af2f9ad8..47a85cda57 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -10,7 +10,7 @@ static const uint32_t BIT_TIME_US = 889; static const uint8_t NBITS = 14; void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { - static bool TOGGLE = false; + static bool toggle = false; dst->set_carrier_frequency(36000); uint64_t out_data = 0; @@ -21,7 +21,7 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { } else { out_data |= 0b11 << 12; } - out_data |= TOGGLE << 11; + out_data |= toggle << 11; out_data |= data.address << 6; out_data |= command; @@ -34,14 +34,14 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { dst->space(BIT_TIME_US); } } - TOGGLE = !TOGGLE; + toggle = !toggle; } optional RC5Protocol::decode(RemoteReceiveData src) { RC5Data out{ .address = 0, .command = 0, }; - int field_bit = 0; + uint8_t field_bit; if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { field_bit = 1; @@ -60,7 +60,7 @@ optional RC5Protocol::decode(RemoteReceiveData src) { return {}; } - uint64_t out_data = 0; + uint32_t out_data = 0; for (int bit = NBITS - 4; bit >= 1; bit--) { 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))) { @@ -78,7 +78,7 @@ optional RC5Protocol::decode(RemoteReceiveData src) { out_data |= 1; } - out.command = (out_data & 0x3F) + (1 - field_bit) * 64; + out.command = (uint8_t)(out_data & 0x3F) + (1 - field_bit) * 64u; out.address = (out_data >> 6) & 0x1F; return out; } diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 7198f2d917..a853c9849e 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -6,7 +6,7 @@ namespace remote_base { static const char *const TAG = "remote_base"; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_block_num) { static rmt_channel_t next_rmt_channel = RMT_CHANNEL_0; this->channel_ = next_rmt_channel; @@ -14,9 +14,9 @@ RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_b } void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { - if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_7) { - this->mem_block_num_ = int(RMT_CHANNEL_7) - int(this->channel_) + 1; - ESP_LOGW(TAG, "Not enough RMT memory blocks avaiable, reduced to %i blocks.", this->mem_block_num_); + 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_); } rmt.channel = this->channel_; rmt.clk_div = this->clock_divider_; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 16c732df83..dd6f7c3482 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -3,11 +3,11 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif @@ -146,13 +146,13 @@ template class RemoteProtocol { class RemoteComponentBase { public: - explicit RemoteComponentBase(GPIOPin *pin) : pin_(pin){}; + explicit RemoteComponentBase(InternalGPIOPin *pin) : pin_(pin){}; protected: - GPIOPin *pin_; + InternalGPIOPin *pin_; }; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 class RemoteRMTChannel { public: explicit RemoteRMTChannel(uint8_t mem_block_num = 1); @@ -161,11 +161,11 @@ class RemoteRMTChannel { void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } protected: - uint32_t from_microseconds(uint32_t us) { + uint32_t from_microseconds_(uint32_t us) { const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; return us * ticks_per_ten_us / 10; } - uint32_t to_microseconds(uint32_t ticks) { + uint32_t to_microseconds_(uint32_t ticks) { const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; return (ticks * 10) / ticks_per_ten_us; } @@ -178,7 +178,7 @@ class RemoteRMTChannel { class RemoteTransmitterBase : public RemoteComponentBase { public: - RemoteTransmitterBase(GPIOPin *pin) : RemoteComponentBase(pin) {} + RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} class TransmitCall { public: explicit TransmitCall(RemoteTransmitterBase *parent) : parent_(parent) {} @@ -221,7 +221,7 @@ class RemoteReceiverDumperBase { class RemoteReceiverBase : public RemoteComponentBase { public: - RemoteReceiverBase(GPIOPin *pin) : RemoteComponentBase(pin) {} + RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); } void register_dumper(RemoteReceiverDumperBase *dumper) { if (dumper->is_secondary()) { diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 5062c46126..4571f332b3 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -1,12 +1,12 @@ #include "samsung_protocol.h" #include "esphome/core/log.h" +#include namespace esphome { namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint8_t NBITS = 32; static const uint32_t HEADER_HIGH_US = 4500; static const uint32_t HEADER_LOW_US = 4500; static const uint32_t BIT_HIGH_US = 560; @@ -17,12 +17,12 @@ static const uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); - dst->reserve(4 + NBITS * 2u); + dst->reserve(4 + data.nbits * 2u); dst->item(HEADER_HIGH_US, HEADER_LOW_US); - for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { - if (data.data & mask) + for (uint8_t bit = data.nbits; bit > 0; bit--) { + if ((data.data >> (bit - 1)) & 1) dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); else dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); @@ -33,16 +33,20 @@ void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { optional SamsungProtocol::decode(RemoteReceiveData src) { SamsungData out{ .data = 0, + .nbits = 0, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; - for (uint8_t i = 0; i < NBITS; i++) { - out.data <<= 1UL; + for (out.nbits = 0; out.nbits < 64; out.nbits++) { if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { - out.data |= 1UL; + out.data = (out.data << 1) | 1; } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - out.data |= 0UL; + out.data = (out.data << 1) | 0; + } else if (out.nbits >= 31) { + if (!src.expect_mark(FOOTER_HIGH_US)) + return {}; + return out; } else { return {}; } @@ -52,7 +56,9 @@ optional SamsungProtocol::decode(RemoteReceiveData src) { return {}; return out; } -void SamsungProtocol::dump(const SamsungData &data) { ESP_LOGD(TAG, "Received Samsung: data=0x%08X", data.data); } +void SamsungProtocol::dump(const SamsungData &data) { + ESP_LOGD(TAG, "Received Samsung: data=0x%" PRIX64 ", nbits=%d", data.data, data.nbits); +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index f7a54788e5..41434f2889 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -7,9 +7,10 @@ namespace esphome { namespace remote_base { struct SamsungData { - uint32_t data; + uint64_t data; + uint8_t nbits; - bool operator==(const SamsungData &rhs) const { return data == rhs.data; } + bool operator==(const SamsungData &rhs) const { return data == rhs.data && nbits == rhs.nbits; } }; class SamsungProtocol : public RemoteProtocol { @@ -23,11 +24,13 @@ DECLARE_REMOTE_PROTOCOL(Samsung) template class SamsungAction : public RemoteTransmitterActionBase { public: - TEMPLATABLE_VALUE(uint32_t, data) + TEMPLATABLE_VALUE(uint64_t, data) + TEMPLATABLE_VALUE(uint8_t, nbits) void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); + data.nbits = this->nbits_.value(x...); SamsungProtocol().encode(dst, data); } }; diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp new file mode 100644 index 0000000000..bd1d2a8f5b --- /dev/null +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -0,0 +1,112 @@ +#include "toshiba_ac_protocol.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.toshibaac"; + +static const uint32_t HEADER_HIGH_US = 4500; +static const uint32_t HEADER_LOW_US = 4500; +static const uint32_t BIT_HIGH_US = 560; +static const uint32_t BIT_ONE_LOW_US = 1690; +static const uint32_t BIT_ZERO_LOW_US = 560; +static const uint32_t FOOTER_HIGH_US = 560; +static const uint32_t FOOTER_LOW_US = 4500; +static const uint16_t PACKET_SPACE = 5500; + +void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { + dst->set_carrier_frequency(38000); + dst->reserve((3 + (48 * 2)) * 3); + + for (uint8_t repeat = 0; repeat < 2; repeat++) { + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint8_t bit = 48; bit > 0; bit--) { + dst->mark(BIT_HIGH_US); + if ((data.rc_code_1 >> (bit - 1)) & 1) + dst->space(BIT_ONE_LOW_US); + else + dst->space(BIT_ZERO_LOW_US); + } + dst->item(FOOTER_HIGH_US, FOOTER_LOW_US); + } + + if (data.rc_code_2 != 0) { + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint8_t bit = 48; bit > 0; bit--) { + dst->mark(BIT_HIGH_US); + if ((data.rc_code_2 >> (bit - 1)) & 1) + dst->space(BIT_ONE_LOW_US); + else + dst->space(BIT_ZERO_LOW_US); + } + dst->item(FOOTER_HIGH_US, FOOTER_LOW_US); + } +} + +optional ToshibaAcProtocol::decode(RemoteReceiveData src) { + uint64_t packet = 0; + ToshibaAcData out{ + .rc_code_1 = 0, + .rc_code_2 = 0, + }; + // *** Packet 1 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + packet = (packet << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + packet = (packet << 1) | 0; + } else { + return {}; + } + } + if (!src.expect_item(FOOTER_HIGH_US, PACKET_SPACE)) + return {}; + + // *** Packet 2 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.rc_code_1 = (out.rc_code_1 << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.rc_code_1 = (out.rc_code_1 << 1) | 0; + } else { + return {}; + } + } + // The first two packets must match + if (packet != out.rc_code_1) + return {}; + // The third packet isn't always present + if (!src.expect_item(FOOTER_HIGH_US, PACKET_SPACE)) + return out; + + // *** Packet 3 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.rc_code_2 = (out.rc_code_2 << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.rc_code_2 = (out.rc_code_2 << 1) | 0; + } else { + return {}; + } + } + + return out; +} + +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); + else + ESP_LOGD(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64, data.rc_code_1); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/toshiba_ac_protocol.h b/esphome/components/remote_base/toshiba_ac_protocol.h new file mode 100644 index 0000000000..c69401c378 --- /dev/null +++ b/esphome/components/remote_base/toshiba_ac_protocol.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct ToshibaAcData { + uint64_t rc_code_1; + uint64_t rc_code_2; + + bool operator==(const ToshibaAcData &rhs) const { return rc_code_1 == rhs.rc_code_1 && rc_code_2 == rhs.rc_code_2; } +}; + +class ToshibaAcProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const ToshibaAcData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(ToshibaAc) + +template class ToshibaAcAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint64_t, rc_code_1) + TEMPLATABLE_VALUE(uint64_t, rc_code_2) + + void encode(RemoteTransmitData *dst, Ts... x) override { + ToshibaAcData data{}; + data.rc_code_1 = this->rc_code_1_.value(x...); + data.rc_code_2 = this->rc_code_2_.value(x...); + ToshibaAcProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index fed52803cb..253204bd1a 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -25,9 +25,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.Schema( { cv.GenerateID(): cv.declare_id(RemoteReceiverComponent), - cv.Required(CONF_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), + 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) @@ -54,7 +52,10 @@ async def to_code(config): else: var = cg.new_Pvariable(config[CONF_ID], pin) - await remote_base.build_dumpers(config[CONF_DUMP]) + dumpers = await remote_base.build_dumpers(config[CONF_DUMP]) + for dumper in dumpers: + cg.add(var.register_dumper(dumper)) + await remote_base.build_triggers(config) await cg.register_component(var, config) diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py index 62c1d9cb27..218b40d6cc 100644 --- a/esphome/components/remote_receiver/binary_sensor.py +++ b/esphome/components/remote_receiver/binary_sensor.py @@ -1,6 +1,4 @@ -import esphome.codegen as cg from esphome.components import binary_sensor, remote_base -from esphome.const import CONF_NAME DEPENDENCIES = ["remote_receiver"] @@ -9,5 +7,4 @@ CONFIG_SCHEMA = remote_base.validate_binary_sensor async def to_code(config): var = await remote_base.build_binary_sensor(config) - cg.add(var.set_name(config[CONF_NAME])) await binary_sensor.register_binary_sensor(var, config) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 25262e3366..50153c105d 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,7 +6,7 @@ namespace esphome { namespace remote_receiver { -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -21,23 +21,23 @@ struct RemoteReceiverComponentStore { bool overflow{false}; uint32_t buffer_size{1000}; uint8_t filter_us{10}; - ISRInternalGPIOPin *pin; + ISRInternalGPIOPin pin; }; #endif class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 , public remote_base::RemoteRMTChannel #endif { public: -#ifdef ARDUINO_ARCH_ESP32 - RemoteReceiverComponent(GPIOPin *pin, uint8_t mem_block_num = 1) +#ifdef USE_ESP32 + RemoteReceiverComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} #else - RemoteReceiverComponent(GPIOPin *pin) : RemoteReceiverBase(pin) {} + RemoteReceiverComponent(InternalGPIOPin *pin) : RemoteReceiverBase(pin) {} #endif void setup() override; void dump_config() override; @@ -49,13 +49,13 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 void decode_rmt_(rmt_item32_t *item, size_t len); RingbufHandle_t ringbuf_; esp_err_t error_code_{ESP_OK}; #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index b2ddc69b1c..dde9b843c9 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -1,7 +1,7 @@ #include "remote_receiver.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include namespace esphome { @@ -20,9 +20,9 @@ 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 = this->from_microseconds_(this->filter_us_); } - rmt.rx_config.idle_threshold = this->from_microseconds(this->idle_us_); + rmt.rx_config.idle_threshold = this->from_microseconds_(this->idle_us_); esp_err_t error = rmt_config(&rmt); if (error != ESP_OK) { @@ -90,14 +90,14 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { ESP_LOGVV(TAG, "START:"); for (size_t i = 0; i < len; 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, "%u A: ON %uus (%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, "%u A: OFF %uus (%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, "%u B: ON %uus (%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, "%u B: OFF %uus (%u ticks)", i, this->to_microseconds_(item[i].duration1), item[i].duration1); } } ESP_LOGVV(TAG, "\n"); @@ -111,16 +111,16 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { } else { if (prev_length > 0) { if (prev_level) { - this->temp_.push_back(this->to_microseconds(prev_length) * multiplier); + this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { - this->temp_.push_back(-int32_t(this->to_microseconds(prev_length)) * multiplier); + this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); } } prev_level = bool(item[i].level0); prev_length = item[i].duration0; } - if (this->to_microseconds(prev_length) > this->idle_us_) { + if (this->to_microseconds_(prev_length) > this->idle_us_) { break; } @@ -131,24 +131,24 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { } else { if (prev_length > 0) { if (prev_level) { - this->temp_.push_back(this->to_microseconds(prev_length) * multiplier); + this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { - this->temp_.push_back(-int32_t(this->to_microseconds(prev_length)) * multiplier); + this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); } } prev_level = bool(item[i].level1); prev_length = item[i].duration1; } - if (this->to_microseconds(prev_length) > this->idle_us_) { + if (this->to_microseconds_(prev_length) > this->idle_us_) { break; } } if (prev_length > 0) { if (prev_level) { - this->temp_.push_back(this->to_microseconds(prev_length) * multiplier); + this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { - this->temp_.push_back(-int32_t(this->to_microseconds(prev_length)) * multiplier); + this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); } } } diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index 3fb68caca2..cf2c15402e 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -1,19 +1,20 @@ #include "remote_receiver.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 namespace esphome { namespace remote_receiver { static const char *const TAG = "remote_receiver.esp8266"; -void ICACHE_RAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { +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(); + const bool level = arg->pin.digital_read(); if (level != next % 2) return; @@ -53,7 +54,7 @@ void RemoteReceiverComponent::setup() { } else { s.buffer_write_at = s.buffer_read_at = 0; } - this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, CHANGE); + this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:"); diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 000fbabfee..733ac5e50d 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -8,13 +8,13 @@ namespace remote_transmitter { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 , public remote_base::RemoteRMTChannel #endif { public: - explicit RemoteTransmitterComponent(GPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} + explicit RemoteTransmitterComponent(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} void setup() override; @@ -26,7 +26,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 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); @@ -34,13 +34,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void space_(uint32_t usec); #endif -#ifdef ARDUINO_ARCH_ESP32 - void configure_rmt(); +#ifdef USE_ESP32 + void configure_rmt_(); uint32_t current_carrier_frequency_{UINT32_MAX}; bool initialized_{false}; std::vector rmt_temp_; esp_err_t error_code_{ESP_OK}; + bool inverted_{false}; #endif uint8_t carrier_duty_percent_{50}; }; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 3d3e26160a..500d7193f3 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -2,14 +2,14 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; -void RemoteTransmitterComponent::setup() {} +void RemoteTransmitterComponent::setup() { this->configure_rmt_(); } void RemoteTransmitterComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Transmitter..."); @@ -27,7 +27,7 @@ void RemoteTransmitterComponent::dump_config() { } } -void RemoteTransmitterComponent::configure_rmt() { +void RemoteTransmitterComponent::configure_rmt_() { rmt_config_t c{}; this->config_rmt(c); @@ -50,6 +50,7 @@ void RemoteTransmitterComponent::configure_rmt() { } else { c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; + this->inverted_ = true; } esp_err_t error = rmt_config(&c); @@ -76,7 +77,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); - this->configure_rmt(); + this->configure_rmt_(); } this->rmt_temp_.clear(); @@ -88,17 +89,17 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen bool level = val >= 0; if (!level) val = -val; - val = this->from_microseconds(static_cast(val)); + val = this->from_microseconds_(static_cast(val)); do { - int32_t item = std::min(val, 32767); + int32_t item = std::min(val, int32_t(32767)); val -= item; if (rmt_i % 2 == 0) { - rmt_item.level0 = static_cast(level); + rmt_item.level0 = static_cast(level ^ this->inverted_); rmt_item.duration0 = static_cast(item); } else { - rmt_item.level1 = static_cast(level); + rmt_item.level1 = static_cast(level ^ this->inverted_); rmt_item.duration1 = static_cast(item); this->rmt_temp_.push_back(rmt_item); } diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index f8735fe763..33c01985d7 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 namespace esphome { namespace remote_transmitter { diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py deleted file mode 100644 index 3a2e43a31a..0000000000 --- a/esphome/components/remote_transmitter/switch.py +++ /dev/null @@ -1,33 +0,0 @@ -import esphome.config_validation as cv -from esphome.components.remote_base import BINARY_SENSOR_REGISTRY -from esphome.util import OrderedDict - - -def show_new(value): - from esphome import yaml_util - - for key in BINARY_SENSOR_REGISTRY: - if key in value: - break - else: - raise cv.Invalid( - "This platform has been removed in 1.13, please see the docs for updated " - "instructions." - ) - - val = value[key] - args = [("platform", "template")] - if "id" in value: - args.append(("id", value["id"])) - if "name" in value: - args.append(("name", value["name"])) - args.append(("turn_on_action", {f"remote_transmitter.transmit_{key}": val})) - - text = yaml_util.dump([OrderedDict(args)]) - raise cv.Invalid( - "This platform has been removed in 1.13, please change to:\n\n{}\n\n." - "".format(text) - ) - - -CONFIG_SCHEMA = show_new diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp index 1380354a5f..4d3dfa5928 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -13,7 +13,7 @@ void ResistanceSensor::dump_config() { ESP_LOGCONFIG(TAG, " Reference Voltage: %.1fV", this->reference_voltage_); } void ResistanceSensor::process_(float value) { - if (isnan(value)) { + if (std::isnan(value)) { this->publish_state(NAN); return; } diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index ca1501195a..329192e902 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_SENSOR, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_OHM, ICON_FLASH, @@ -25,7 +24,10 @@ CONFIGURATIONS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_OHM, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/restart_switch.cpp index ea46c5f910..3076fde99e 100644 --- a/esphome/components/restart/restart_switch.cpp +++ b/esphome/components/restart/restart_switch.cpp @@ -1,4 +1,5 @@ #include "restart_switch.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 2fa4eb05c5..9156d995bc 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -85,8 +85,7 @@ class RFBridgeReceivedCodeTrigger : public Trigger { class RFBridgeReceivedAdvancedCodeTrigger : public Trigger { public: explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_advanced_code_received_callback( - [this](RFBridgeAdvancedData data) { this->trigger(std::move(data)); }); + parent->add_on_advanced_code_received_callback([this](const RFBridgeAdvancedData &data) { this->trigger(data); }); } }; diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index 1a3bf9f614..ef53c8042d 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -15,8 +15,7 @@ class RGBLightOutput : public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbct/__init__.py b/esphome/components/rgbct/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/rgbct/light.py b/esphome/components/rgbct/light.py new file mode 100644 index 0000000000..0565057316 --- /dev/null +++ b/esphome/components/rgbct/light.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light, output +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_INTERLOCK, + CONF_COLOR_TEMPERATURE, + CONF_GREEN, + CONF_RED, + CONF_OUTPUT_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) + +CODEOWNERS = ["@jesserockz"] + +rgbct_ns = cg.esphome_ns.namespace("rgbct") +RGBCTLightOutput = rgbct_ns.class_("RGBCTLightOutput", light.LightOutput) + +CONF_WHITE_BRIGHTNESS = "white_brightness" + +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBCTLightOutput), + cv.Required(CONF_RED): cv.use_id(output.FloatOutput), + cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), + cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLOR_TEMPERATURE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WHITE_BRIGHTNESS): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, + } + ), + light.validate_color_temperature_channels, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + red = await cg.get_variable(config[CONF_RED]) + cg.add(var.set_red(red)) + green = await cg.get_variable(config[CONF_GREEN]) + cg.add(var.set_green(green)) + blue = await cg.get_variable(config[CONF_BLUE]) + cg.add(var.set_blue(blue)) + + color_temp = await cg.get_variable(config[CONF_COLOR_TEMPERATURE]) + cg.add(var.set_color_temperature(color_temp)) + white_brightness = await cg.get_variable(config[CONF_WHITE_BRIGHTNESS]) + cg.add(var.set_white_brightness(white_brightness)) + + cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h new file mode 100644 index 0000000000..9257d67cd1 --- /dev/null +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/components/light/color_mode.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace rgbct { + +class RGBCTLightOutput : public light::LightOutput { + public: + void set_red(output::FloatOutput *red) { red_ = red; } + void set_green(output::FloatOutput *green) { green_ = green; } + void set_blue(output::FloatOutput *blue) { blue_ = blue; } + + void set_color_temperature(output::FloatOutput *color_temperature) { color_temperature_ = color_temperature; } + void set_white_brightness(output::FloatOutput *white_brightness) { white_brightness_ = white_brightness; } + + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); + 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; + } + void write_state(light::LightState *state) override { + float red, green, blue, color_temperature, white_brightness; + + state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &white_brightness); + + this->red_->set_level(red); + this->green_->set_level(green); + this->blue_->set_level(blue); + this->color_temperature_->set_level(color_temperature); + this->white_brightness_->set_level(white_brightness); + } + + protected: + output::FloatOutput *red_; + output::FloatOutput *green_; + output::FloatOutput *blue_; + output::FloatOutput *color_temperature_; + output::FloatOutput *white_brightness_; + float cold_white_temperature_; + float warm_white_temperature_; + bool color_interlock_{true}; +}; + +} // namespace rgbct +} // namespace esphome diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py index de26edf7d5..f747580f61 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -1,11 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output -from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_WHITE +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_INTERLOCK, + CONF_GREEN, + CONF_RED, + CONF_OUTPUT_ID, + CONF_WHITE, +) rgbw_ns = cg.esphome_ns.namespace("rgbw") RGBWLightOutput = rgbw_ns.class_("RGBWLightOutput", light.LightOutput) -CONF_COLOR_INTERLOCK = "color_interlock" CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( { diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 90a650851b..0f55775608 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -16,10 +16,10 @@ 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(); - traits.set_supports_brightness(true); - traits.set_supports_color_interlock(this->color_interlock_); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + 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/light.py b/esphome/components/rgbww/light.py index d152fbc6db..35f77b154b 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -3,6 +3,8 @@ import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( CONF_BLUE, + CONF_COLOR_INTERLOCK, + CONF_CONSTANT_BRIGHTNESS, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, @@ -15,22 +17,26 @@ from esphome.const import ( rgbww_ns = cg.esphome_ns.namespace("rgbww") RGBWWLightOutput = rgbww_ns.class_("RGBWWLightOutput", light.LightOutput) -CONF_CONSTANT_BRIGHTNESS = "constant_brightness" -CONF_COLOR_INTERLOCK = "color_interlock" -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), - cv.Required(CONF_RED): cv.use_id(output.FloatOutput), - cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), - cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, - cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, - } +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), + cv.Required(CONF_RED): cv.use_id(output.FloatOutput), + cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), + cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, + } + ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), + light.validate_color_temperature_channels, ) @@ -47,10 +53,17 @@ async def to_code(config): cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index e14b967530..5a86b88595 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -14,25 +14,23 @@ class RGBWWLightOutput : public light::LightOutput { void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_cold_white(output::FloatOutput *cold_white) { cold_white_ = cold_white; } void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } - void set_cold_white_temperature(int cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } - void set_warm_white_temperature(int warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); - traits.set_supports_color_interlock(this->color_interlock_); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); + 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; } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_, - this->color_interlock_); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -46,8 +44,8 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *blue_; output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - int cold_white_temperature_; - int warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; bool color_interlock_{false}; }; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 8ef6f932c5..7c95fac98e 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -82,12 +82,12 @@ static const uint16_t DRAM_ATTR STATE_LOOKUP_TABLE[32] = { STATE_CW | STATE_S3 // 0x1F: stay here }; -void ICACHE_RAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore *arg) { +void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore *arg) { // Forget upper bits and add pin states uint8_t input_state = arg->state & STATE_LUT_MASK; - if (arg->pin_a->digital_read()) + if (arg->pin_a.digital_read()) input_state |= STATE_PIN_A_HIGH; - if (arg->pin_b->digital_read()) + if (arg->pin_b.digital_read()) input_state |= STATE_PIN_B_HIGH; int8_t rotation_dir = 0; @@ -134,8 +134,8 @@ void RotaryEncoderSensor::setup() { this->pin_i_->setup(); } - this->pin_a_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); - this->pin_b_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); + this->pin_a_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); + this->pin_b_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } void RotaryEncoderSensor::dump_config() { LOG_SENSOR("", "Rotary Encoder", this); @@ -157,13 +157,14 @@ void RotaryEncoderSensor::dump_config() { void RotaryEncoderSensor::loop() { std::array rotation_events; bool rotation_events_overflow; - ets_intr_lock(); - rotation_events = this->store_.rotation_events; - rotation_events_overflow = this->store_.rotation_events_overflow; + { + InterruptLock lock; + rotation_events = this->store_.rotation_events; + rotation_events_overflow = this->store_.rotation_events_overflow; - this->store_.rotation_events.fill(0); - this->store_.rotation_events_overflow = false; - ets_intr_unlock(); + this->store_.rotation_events.fill(0); + this->store_.rotation_events_overflow = false; + } if (rotation_events_overflow) { ESP_LOGW(TAG, "Captured more rotation events than expected"); diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 000350d66c..4825e472a1 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -3,7 +3,7 @@ #include #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" @@ -19,8 +19,8 @@ enum RotaryEncoderResolution { }; struct RotaryEncoderSensorStore { - ISRInternalGPIOPin *pin_a; - ISRInternalGPIOPin *pin_b; + ISRInternalGPIOPin pin_a; + ISRInternalGPIOPin pin_b; volatile int32_t counter{0}; RotaryEncoderResolution resolution{ROTARY_ENCODER_1_PULSE_PER_CYCLE}; @@ -37,8 +37,8 @@ struct RotaryEncoderSensorStore { class RotaryEncoderSensor : public sensor::Sensor, public Component { public: - void set_pin_a(GPIOPin *pin_a) { pin_a_ = pin_a; } - void set_pin_b(GPIOPin *pin_b) { pin_b_ = pin_b; } + void set_pin_a(InternalGPIOPin *pin_a) { pin_a_ = pin_a; } + void set_pin_b(InternalGPIOPin *pin_b) { pin_b_ = pin_b; } /** Set the resolution of the rotary encoder. * @@ -76,8 +76,8 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { } protected: - GPIOPin *pin_a_; - GPIOPin *pin_b_; + InternalGPIOPin *pin_a_; + InternalGPIOPin *pin_b_; GPIOPin *pin_i_{nullptr}; /// Index pin, if this is not nullptr, the counter will reset to 0 once this pin is HIGH. RotaryEncoderSensorStore store_{}; diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 079f00d284..ef1110c6d8 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_STEPS, ICON_ROTATE_RIGHT, @@ -50,25 +49,23 @@ def validate_min_max_value(config): max_val = config[CONF_MAX_VALUE] if min_val >= max_val: raise cv.Invalid( - "Max value {} must be smaller than min value {}" - "".format(max_val, min_val) + f"Max value {max_val} must be smaller than min value {min_val}" ) return config CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_STEPS, ICON_ROTATE_RIGHT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_STEPS, + icon=ICON_ROTATE_RIGHT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { cv.GenerateID(): cv.declare_id(RotaryEncoderSensor), - cv.Required(CONF_PIN_A): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), - cv.Required(CONF_PIN_B): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), + cv.Required(CONF_PIN_A): cv.All(pins.internal_gpio_input_pin_schema), + cv.Required(CONF_PIN_B): cv.All(pins.internal_gpio_input_pin_schema), cv.Optional(CONF_PIN_RESET): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RESOLUTION, default=1): cv.enum(RESOLUTIONS, int=True), cv.Optional(CONF_MIN_VALUE): cv.int_, diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 7f860fe3d7..e9453896ac 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -1,6 +1,7 @@ import logging import esphome.codegen as cg 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 @@ -36,12 +37,8 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate(config, item_config): - # Not adding this to FloatOutput as this is the only component which needs `update_frequency` - - parent_config = config.get_config_by_id(item_config[CONF_OUTPUT]) - platform = parent_config[CONF_PLATFORM] - +def validate_parent_output_config(value): + platform = value.get(CONF_PLATFORM) PWM_GOOD = ["esp8266_pwm", "ledc"] PWM_BAD = [ "ac_dimmer ", @@ -55,14 +52,25 @@ def validate(config, item_config): ] if platform in PWM_BAD: - raise ValueError(f"Component rtttl cannot use {platform} as output component") + raise cv.Invalid(f"Component rtttl cannot use {platform} as output component") if platform not in PWM_GOOD: _LOGGER.warning( - "Component rtttl is not known to work with the selected output type. Make sure this output supports custom frequency output method." + "Component rtttl is not known to work with the selected output type. " + "Make sure this output supports custom frequency output method." ) +FINAL_VALIDATE_SCHEMA = cv.Schema( + { + cv.Required(CONF_OUTPUT): fv.id_declaration_match_schema( + validate_parent_output_config + ) + }, + extra=cv.ALLOW_EXTRA, +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 84cc2ce48c..d571c2f287 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -1,4 +1,5 @@ #include "rtttl.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index e3e9f42305..bdd012cf5c 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -1,7 +1,7 @@ #include "ruuvi_ble.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvi_ble { diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.h b/esphome/components/ruuvi_ble/ruuvi_ble.h index 848004f3d7..add431ce42 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.h +++ b/esphome/components/ruuvi_ble/ruuvi_ble.h @@ -3,7 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvi_ble { diff --git a/esphome/components/ruuvitag/ruuvitag.cpp b/esphome/components/ruuvitag/ruuvitag.cpp index f4e4a72270..9b462b4794 100644 --- a/esphome/components/ruuvitag/ruuvitag.cpp +++ b/esphome/components/ruuvitag/ruuvitag.cpp @@ -1,7 +1,7 @@ #include "ruuvitag.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvitag { diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index 863c5775c2..63029ebb4d 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/ruuvi_ble/ruuvi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace ruuvitag { diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 12b8425d14..342a5eff24 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -14,13 +14,11 @@ from esphome.const import ( CONF_TX_POWER, CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_CELSIUS, @@ -29,7 +27,6 @@ from esphome.const import ( UNIT_HECTOPASCAL, UNIT_G, UNIT_DECIBEL_MILLIWATT, - UNIT_EMPTY, ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, @@ -52,69 +49,68 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(RuuviTag), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 2, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_X, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_X, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Y, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Y, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Z, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Z, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TX_POWER): sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py new file mode 100644 index 0000000000..f150d6e086 --- /dev/null +++ b/esphome/components/safe_mode/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@paulmonigatti"] + +safe_mode_ns = cg.esphome_ns.namespace("safe_mode") diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py new file mode 100644 index 0000000000..0ad814ff4f --- /dev/null +++ b/esphome/components/safe_mode/switch/__init__.py @@ -0,0 +1,36 @@ +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_ID, + CONF_INVERTED, + CONF_ICON, + CONF_OTA, + ICON_RESTART_ALERT, +) +from .. import safe_mode_ns + +DEPENDENCIES = ["ota"] + +SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SafeModeSwitch), + cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent), + cv.Optional(CONF_INVERTED): cv.invalid( + "Safe Mode Restart switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): switch.icon, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + ota = await cg.get_variable(config[CONF_OTA]) + cg.add(var.set_ota(ota)) diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp new file mode 100644 index 0000000000..a3979eec06 --- /dev/null +++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp @@ -0,0 +1,29 @@ +#include "safe_mode_switch.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"; + +void SafeModeSwitch::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; } + +void SafeModeSwitch::write_state(bool state) { + // Acknowledge + this->publish_state(false); + + if (state) { + ESP_LOGI(TAG, "Restarting device in safe mode..."); + this->ota_->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 +} // namespace esphome diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h new file mode 100644 index 0000000000..2772db3d84 --- /dev/null +++ b/esphome/components/safe_mode/switch/safe_mode_switch.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ota/ota_component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace safe_mode { + +class SafeModeSwitch : public switch_::Switch, public Component { + public: + void dump_config() override; + void set_ota(ota::OTAComponent *ota); + + protected: + ota::OTAComponent *ota_; + void write_state(bool state) override; +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3eda98d41d..d1246d9766 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -1,5 +1,10 @@ #include "scd30.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include +#endif namespace esphome { namespace scd30 { @@ -23,7 +28,7 @@ static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; void SCD30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up scd30..."); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 Wire.setClockStretchLimit(150000); #endif @@ -51,11 +56,11 @@ void SCD30Component::setup() { return; } } -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 // According ESP32 clock stretching is typically 30ms and up to 150ms "due to // internal calibration processes". The I2C peripheral only supports 13ms (at // least when running at 80MHz). - // In practise it seems that clock stretching occures during this calibration + // In practise it seems that clock stretching occurs during this calibration // calls. It also seems that delays in between calls makes them // disappear/shorter. Hence work around with delays for ESP32. // @@ -73,7 +78,7 @@ void SCD30Component::setup() { return; } } -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 delay(30); #endif @@ -83,7 +88,7 @@ void SCD30Component::setup() { this->mark_failed(); return; } -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 delay(30); #endif @@ -193,7 +198,7 @@ bool SCD30Component::write_command_(uint16_t command, uint16_t data) { raw[2] = data >> 8; raw[3] = data & 0xFF; raw[4] = sht_crc_(raw[2], raw[3]); - return this->write_bytes_raw(raw, 5); + return this->write(raw, 5) == i2c::ERROR_OK; } uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { @@ -221,10 +226,9 @@ uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { bool SCD30Component::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -233,13 +237,11 @@ bool SCD30Component::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sht_crc_(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 7a08289474..c0317c96e0 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,13 +3,11 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, @@ -33,25 +31,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SCD30Component), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All( diff --git a/esphome/components/scd4x/__init__.py b/esphome/components/scd4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp new file mode 100644 index 0000000000..c91fd5e882 --- /dev/null +++ b/esphome/components/scd4x/scd4x.cpp @@ -0,0 +1,259 @@ +#include "scd4x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace scd4x { + +static const char *const TAG = "scd4x"; + +static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682; +static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d; +static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; +static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; +static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; +static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; +static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; +static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; +static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; +static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; + +static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; + +void SCD4XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up scd4x..."); + + // the sensor needs 1000 ms to enter the idle state + this->set_timeout(1000, [this]() { + // Check if measurement is ready before reading the value + if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { + ESP_LOGE(TAG, "Failed to write data ready status command"); + this->mark_failed(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1)) { + ESP_LOGE(TAG, "Failed to read data ready status"); + this->mark_failed(); + return; + } + + // In order to query the device periodic measurement must be ceased + if (raw_read_status[0]) { + ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + if (!this->write_command_(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->mark_failed(); + return; + } + } + + if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) { + ESP_LOGE(TAG, "Failed to write get serial command"); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[3]; + if (!this->read_data_(raw_serial_number, 3)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), + uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); + + if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, + (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { + ESP_LOGE(TAG, "Error setting temperature offset."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + // If pressure compensation available use it + // else use altitude + if (ambient_pressure_compensation_) { + if (!this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, ambient_pressure_compensation_)) { + ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } else { + if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + ESP_LOGE(TAG, "Error setting altitude compensation."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } + + if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { + ESP_LOGE(TAG, "Error setting automatic self calibration."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + // Finally start sensor measurements + if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { + ESP_LOGE(TAG, "Error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + initialized_ = true; + ESP_LOGD(TAG, "Sensor initialized"); + }); +} + +void SCD4XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "scd4x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); + if (this->ambient_pressure_compensation_) { + ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + } else { + ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void SCD4XComponent::update() { + if (!initialized_) { + return; + } + + // Check if data is ready + if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } + + if (!this->write_command_(SCD4X_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; + } + + // Read off sensor data + uint16_t raw_data[3]; + if (!this->read_data_(raw_data, 3)) { + this->status_set_warning(); + return; + } + + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(raw_data[0]); + + if (this->temperature_sensor_ != nullptr) { + const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); + this->temperature_sensor_->publish_state(temperature); + } + + if (this->humidity_sensor_ != nullptr) { + const float humidity = (100.0f * raw_data[2]) / (1 << 16); + this->humidity_sensor_->publish_state(humidity); + } + + this->status_clear_warning(); +} + +uint8_t SCD4XComponent::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SCD4XComponent::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + std::vector buf(num_bytes); + + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + return true; +} + +bool SCD4XComponent::write_command_(uint16_t command) { + const uint8_t num_bytes = 2; + uint8_t buffer[num_bytes]; + + buffer[0] = (command >> 8); + buffer[1] = command & 0xff; + + return this->write(buffer, num_bytes) == i2c::ERROR_OK; +} + +bool SCD4XComponent::write_command_(uint16_t command, uint16_t data) { + uint8_t raw[5]; + raw[0] = command >> 8; + raw[1] = command & 0xFF; + raw[2] = data >> 8; + raw[3] = data & 0xFF; + raw[4] = sht_crc_(raw[2], raw[3]); + return this->write(raw, 5) == i2c::ERROR_OK; +} + +} // namespace scd4x +} // namespace esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h new file mode 100644 index 0000000000..3c428b8623 --- /dev/null +++ b/esphome/components/scd4x/scd4x.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace scd4x { + +enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; + +class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } + void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } + void set_ambient_pressure_compensation(float pressure) { + ambient_pressure_compensation_ = true; + ambient_pressure_ = (uint16_t)(pressure * 1000); + } + void set_temperature_offset(float offset) { temperature_offset_ = offset; }; + + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + + protected: + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + bool read_data_(uint16_t *data, uint8_t len); + bool write_command_(uint16_t command); + bool write_command_(uint16_t command, uint16_t data); + + ERRORCODE error_code_; + + bool initialized_{false}; + + float temperature_offset_; + uint16_t altitude_compensation_; + bool ambient_pressure_compensation_; + uint16_t ambient_pressure_; + bool enable_asc_; + + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace scd4x +} // namespace esphome diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py new file mode 100644 index 0000000000..0b1a960f6f --- /dev/null +++ b/esphome/components/scd4x/sensor.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor + +from esphome.const import ( + CONF_ID, + CONF_CO2, + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + ICON_MOLECULE_CO2, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +scd4x_ns = cg.esphome_ns.namespace("scd4x") +SCD4XComponent = scd4x_ns.class_("SCD4XComponent", cg.PollingComponent, i2c.I2CDevice) + +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 = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SCD4XComponent), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_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, + ), + 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_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, + cv.Optional(CONF_ALTITUDE_COMPENSATION, default="0m"): cv.All( + cv.float_with_unit("altitude", "(m|m a.s.l.|MAMSL|MASL)"), + cv.int_range(min=0, max=0xFFFF, max_included=False), + ), + cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure, + cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x62)) +) + +SENSOR_MAP = { + CONF_CO2: "set_co2_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_HUMIDITY: "set_humidity_sensor", +} + +SETTING_MAP = { + CONF_AUTOMATIC_SELF_CALIBRATION: "set_automatic_self_calibration", + CONF_ALTITUDE_COMPENSATION: "set_altitude_compensation", + CONF_AMBIENT_PRESSURE_COMPENSATION: "set_ambient_pressure_compensation", + CONF_TEMPERATURE_OFFSET: "set_temperature_offset", +} + + +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 SETTING_MAP.items(): + if key in config: + cg.add(getattr(var, funcName)(config[key])) + + for key, funcName in SENSOR_MAP.items(): + + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 43356c0036..9702878475 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -50,7 +50,7 @@ def assign_declare_id(value): CONFIG_SCHEMA = automation.validate_automation( { # Don't declare id as cv.declare_id yet, because the ID type - # dpeends on the mode. Will be checked later with assign_declare_id + # depends on the mode. Will be checked later with assign_declare_id cv.Required(CONF_ID): cv.string_strict, cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of( *SCRIPT_MODES, lower=True diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 64db6b80e7..5663d32ce8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -65,7 +65,7 @@ class QueueingScript : public Script, public Component { /** A script type that executes new instances in parallel. * * If a new instance is started while previous ones haven't finished yet, - * the new one is exeucted in parallel to the other instances. + * the new one is executed in parallel to the other instances. */ class ParallelScript : public Script { public: diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 39ef280fef..8a0d9674a7 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -17,26 +17,23 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, ICON_FLASH, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_DEGREES, - UNIT_EMPTY, UNIT_HERTZ, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_VOLT_AMPS, UNIT_VOLT_AMPS_REACTIVE, - UNIT_VOLT_AMPS_REACTIVE_HOURS, UNIT_WATT, - UNIT_WATT_HOURS, ) AUTO_LOAD = ["modbus"] @@ -46,27 +43,44 @@ sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter") SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice) PHASE_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_PHASE_ANGLE: sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 ), - CONF_PHASE_ANGLE: sensor.sensor_schema(UNIT_DEGREES, ICON_FLASH, 3), } PHASE_SCHEMA = cv.Schema( @@ -81,31 +95,34 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/sdp3x/__init__.py b/esphome/components/sdp3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp new file mode 100644 index 0000000000..ba7a028f8e --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -0,0 +1,124 @@ +#include "sdp3x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace sdp3x { + +static const char *const TAG = "sdp3x.sensor"; +static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; +static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; +static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; +static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; +static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; + +void SDP3XComponent::update() { this->read_pressure_(); } + +void SDP3XComponent::setup() { + ESP_LOGD(TAG, "Setting up SDP3X..."); + + if (this->write(SDP3X_STOP_MEAS, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason + } + + if (this->write(SDP3X_SOFT_RESET, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason + } + + delay_microseconds_accurate(20000); + + if (this->write(SDP3X_READ_ID1, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); + this->mark_failed(); + return; + } + if (this->write(SDP3X_READ_ID2, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read ID2 SDP3X failed!"); + this->mark_failed(); + return; + } + + uint8_t data[18]; + if (this->read(data, 18) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) { + ESP_LOGE(TAG, "CRC ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (data[3] == 0x01) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP31"); + pressure_scale_factor_ = 60.0f * 100.0f; // Scale factors converted to hPa per count + } else if (data[3] == 0x02) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP32"); + pressure_scale_factor_ = 240.0f * 100.0f; + } + + if (this->write(SDP3X_START_DP_AVG, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "SDP3X started!"); +} +void SDP3XComponent::dump_config() { + LOG_SENSOR(" ", "SDP3X", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, " Connection with SDP3X failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void SDP3XComponent::read_pressure_() { + uint8_t data[9]; + if (this->read(data, 9) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Couldn't read SDP3X data!"); + this->status_set_warning(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { + ESP_LOGW(TAG, "Invalid SDP3X data!"); + this->status_set_warning(); + return; + } + + int16_t pressure_raw = encode_uint16(data[0], data[1]); + float pressure = pressure_raw / pressure_scale_factor_; + ESP_LOGV(TAG, "Got raw pressure=%d, scale factor =%.3f ", pressure_raw, pressure_scale_factor_); + ESP_LOGD(TAG, "Got Pressure=%.3f hPa", pressure); + + this->publish_state(pressure); + this->status_clear_warning(); +} + +float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } + +// Check CRC function from SDP3X sample code provided by sensirion +// Returns true if a checksum is OK +bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) { + uint8_t crc = 0xFF; + + // calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1) + for (int i = 0; i < size; i++) { + crc ^= (data[i]); + for (uint8_t bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + // verify checksum + return (crc == checksum); +} + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h new file mode 100644 index 0000000000..51c9973c61 --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.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 sdp3x { + +class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Schedule temperature+pressure readings. + void update() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + /// Internal method to read the pressure from the component after it has been scheduled. + void read_pressure_(); + + bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum); + + float pressure_scale_factor_ = 0.0f; // hPa per count +}; + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py new file mode 100644 index 0000000000..08d7250f6e --- /dev/null +++ b/esphome/components/sdp3x/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] + +sdp3x_ns = cg.esphome_ns.namespace("sdp3x") +SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=3, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(SDP3XComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .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) + await sensor.register_sensor(var, config) diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index af482839a9..456d47ee91 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -7,7 +7,8 @@ from esphome.const import ( CONF_PM_2_5, CONF_RX_ONLY, CONF_UPDATE_INTERVAL, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -39,18 +40,18 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SDS011Component), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_minutes, diff --git a/esphome/components/selec_meter/__init__.py b/esphome/components/selec_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp new file mode 100644 index 0000000000..8bcf91f3c0 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -0,0 +1,108 @@ +#include "selec_meter.h" +#include "selec_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace selec_meter { + +static const char *const TAG = "selec_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers + +void SelecMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SelecMeter!"); + return; + } + + auto selec_meter_get_float = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]); + + float f; + memcpy(&f, &temp, sizeof(f)); + return (f * unit); + }; + + float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT); + float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT); + float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT); + float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT); + float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT); + float maximum_demand_active_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_reactive_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_apparent_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + + if (this->total_active_energy_sensor_ != nullptr) + this->total_active_energy_sensor_->publish_state(total_active_energy); + if (this->import_active_energy_sensor_ != nullptr) + this->import_active_energy_sensor_->publish_state(import_active_energy); + if (this->export_active_energy_sensor_ != nullptr) + this->export_active_energy_sensor_->publish_state(export_active_energy); + if (this->total_reactive_energy_sensor_ != nullptr) + this->total_reactive_energy_sensor_->publish_state(total_reactive_energy); + if (this->import_reactive_energy_sensor_ != nullptr) + this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); + if (this->export_reactive_energy_sensor_ != nullptr) + this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); + if (this->apparent_energy_sensor_ != nullptr) + this->apparent_energy_sensor_->publish_state(apparent_energy); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->apparent_power_sensor_ != nullptr) + this->apparent_power_sensor_->publish_state(apparent_power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->maximum_demand_active_power_sensor_ != nullptr) + this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power); + if (this->maximum_demand_reactive_power_sensor_ != nullptr) + this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power); + if (this->maximum_demand_apparent_power_sensor_ != nullptr) + this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); +} + +void SelecMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SelecMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SELEC Meter:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); + LOG_SENSOR(" ", "Apparent Energy", this->apparent_energy_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Maximum Demand Active Power", this->maximum_demand_active_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Reactive Power", this->maximum_demand_reactive_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_); +} + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h new file mode 100644 index 0000000000..0477cd2a62 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace selec_meter { + +#define SELEC_METER_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; } + +class SelecMeter : public PollingComponent, public modbus::ModbusDevice { + public: + SELEC_METER_SENSOR(total_active_energy) + SELEC_METER_SENSOR(import_active_energy) + SELEC_METER_SENSOR(export_active_energy) + SELEC_METER_SENSOR(total_reactive_energy) + SELEC_METER_SENSOR(import_reactive_energy) + SELEC_METER_SENSOR(export_reactive_energy) + SELEC_METER_SENSOR(apparent_energy) + SELEC_METER_SENSOR(active_power) + SELEC_METER_SENSOR(reactive_power) + SELEC_METER_SENSOR(apparent_power) + SELEC_METER_SENSOR(voltage) + SELEC_METER_SENSOR(current) + SELEC_METER_SENSOR(power_factor) + SELEC_METER_SENSOR(frequency) + SELEC_METER_SENSOR(maximum_demand_active_power) + SELEC_METER_SENSOR(maximum_demand_reactive_power) + SELEC_METER_SENSOR(maximum_demand_apparent_power) + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; +}; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter_registers.h b/esphome/components/selec_meter/selec_meter_registers.h new file mode 100644 index 0000000000..dfaf65ff08 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter_registers.h @@ -0,0 +1,32 @@ +#pragma once + +namespace esphome { +namespace selec_meter { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; +static const float MULTIPLY_THOUSAND_UNIT = 1000; + +/* PHASE STATUS REGISTERS */ +static const uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000; +static const uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002; +static const uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004; +static const uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006; +static const uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008; +static const uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A; +static const uint16_t SELEC_APPARENT_ENERGY = 0x000C; +static const uint16_t SELEC_ACTIVE_POWER = 0x000E; +static const uint16_t SELEC_REACTIVE_POWER = 0x0010; +static const uint16_t SELEC_APPARENT_POWER = 0x0012; +static const uint16_t SELEC_VOLTAGE = 0x0014; +static const uint16_t SELEC_CURRENT = 0x0016; +static const uint16_t SELEC_POWER_FACTOR = 0x0018; +static const uint16_t SELEC_FREQUENCY = 0x001A; +static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C; +static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E; +static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py new file mode 100644 index 0000000000..168d3a3db2 --- /dev/null +++ b/esphome/components/selec_meter/sensor.py @@ -0,0 +1,173 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_EXPORT_ACTIVE_ENERGY, + CONF_EXPORT_REACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_IMPORT_ACTIVE_ENERGY, + CONF_IMPORT_REACTIVE_ENERGY, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +CONF_TOTAL_ACTIVE_ENERGY = "total_active_energy" +CONF_TOTAL_REACTIVE_ENERGY = "total_reactive_energy" +CONF_APPARENT_ENERGY = "apparent_energy" +CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power" +CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power" +CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power" + +UNIT_KILOWATT_HOURS = "kWh" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" + +selec_meter_ns = cg.esphome_ns.namespace("selec_meter") +SelecMeter = selec_meter_ns.class_( + "SelecMeter", cg.PollingComponent, modbus.ModbusDevice +) + +SENSORS = { + CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_APPARENT_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_POWER_FACTOR: sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), +} + +CONFIG_SCHEMA = ( + cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)}) + .extend( + {cv.Optional(sensor_name): schema for sensor_name, schema in SENSORS.items()} + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + for name in SENSORS: + if name in config: + sens = await sensor.new_sensor(config[name]) + cg.add(getattr(var, f"set_{name}_sensor")(sens)) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py new file mode 100644 index 0000000000..c156a63a86 --- /dev/null +++ b/esphome/components/select/__init__.py @@ -0,0 +1,95 @@ +from typing import List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ID, + CONF_ON_VALUE, + CONF_OPTION, + CONF_TRIGGER_ID, + CONF_MQTT_ID, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +select_ns = cg.esphome_ns.namespace("select") +Select = select_ns.class_("Select", cg.EntityBase) +SelectPtr = Select.operator("ptr") + +# Triggers +SelectStateTrigger = select_ns.class_( + "SelectStateTrigger", automation.Trigger.template(cg.float_) +) + +# Actions +SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) + +icon = cv.icon + + +SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_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), + } + ), + } +) + + +async def setup_select_core_(var, config, *, options: List[str]): + await setup_entity(var, config) + + cg.add(var.traits.set_options(options)) + + 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 CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_select(var, config, *, options: List[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_select(var)) + await setup_select_core_(var, config, options=options) + + +async def new_select(config, *, options: List[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_select(var, config, options=options) + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_SELECT") + cg.add_global(select_ns.using) + + +@automation.register_action( + "select.set", + SelectSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), + } + ), +) +async def select_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_OPTION], args, str) + cg.add(var.set_option(template_)) + return var diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h new file mode 100644 index 0000000000..1e0bfed63d --- /dev/null +++ b/esphome/components/select/automation.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "select.h" + +namespace esphome { +namespace select { + +class SelectStateTrigger : public Trigger { + public: + explicit SelectStateTrigger(Select *parent) { + parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + } +}; + +template class SelectSetAction : public Action { + public: + SelectSetAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(std::string, option) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_option(this->option_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp new file mode 100644 index 0000000000..14f4d9277d --- /dev/null +++ b/esphome/components/select/select.cpp @@ -0,0 +1,43 @@ +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace select { + +static const char *const TAG = "select"; + +void SelectCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (!this->option_.has_value()) { + ESP_LOGW(TAG, "No value set for SelectCall"); + return; + } + + const auto &traits = this->parent_->traits; + auto value = *this->option_; + auto options = traits.get_options(); + + if (std::find(options.begin(), options.end(), value) == options.end()) { + ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str()); + return; + } + + ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str()); + this->parent_->control(*this->option_); +} + +void Select::publish_state(const std::string &state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); + this->state_callback_.call(state); +} + +void Select::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +uint32_t Select::hash_base() { return 2812997003UL; } + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h new file mode 100644 index 0000000000..6113cca1fd --- /dev/null +++ b/esphome/components/select/select.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +#define LOG_SELECT(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 Select; + +class SelectCall { + public: + explicit SelectCall(Select *parent) : parent_(parent) {} + void perform(); + + SelectCall &set_option(const std::string &option) { + option_ = option; + return *this; + } + const optional &get_option() const { return option_; } + + protected: + Select *const parent_; + optional option_; +}; + +class SelectTraits { + public: + void set_options(std::vector options) { this->options_ = std::move(options); } + const std::vector get_options() const { return this->options_; } + + protected: + std::vector options_; +}; + +/** Base-class for all selects. + * + * A select can use publish_state to send out a new value. + */ +class Select : public EntityBase { + public: + std::string state; + + void publish_state(const std::string &state); + + SelectCall make_call() { return SelectCall(this); } + void set(const std::string &value) { make_call().set_option(value).perform(); } + + void add_on_state_callback(std::function &&callback); + + SelectTraits traits; + + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_state_; } + + protected: + friend class SelectCall; + + /** Set the value of the select, this is a virtual method that each select integration must implement. + * + * This method is called by the SelectCall. + * + * @param value The value as validated by the SelectCall. + */ + virtual void control(const std::string &value) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_; + bool has_state_{false}; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 8fbb6f69db..610892dd9e 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -78,9 +78,12 @@ uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { } void SenseAirComponent::background_calibration() { + // Responses are just echoes but must be read to clear the buffer ESP_LOGD(TAG, "SenseAir Starting background calibration"); - this->senseair_write_command_(SENSEAIR_COMMAND_CLEAR_ACK_REGISTER, nullptr, 0); - this->senseair_write_command_(SENSEAIR_COMMAND_BACKGROUND_CAL, nullptr, 0); + uint8_t command_length = sizeof(SENSEAIR_COMMAND_CLEAR_ACK_REGISTER) / sizeof(SENSEAIR_COMMAND_CLEAR_ACK_REGISTER[0]); + uint8_t response[command_length]; + this->senseair_write_command_(SENSEAIR_COMMAND_CLEAR_ACK_REGISTER, response, command_length); + this->senseair_write_command_(SENSEAIR_COMMAND_BACKGROUND_CAL, response, command_length); } void SenseAirComponent::background_calibration_result() { @@ -98,18 +101,25 @@ void SenseAirComponent::background_calibration_result() { return; } - ESP_LOGD(TAG, "SenseAir Result=%s (%02x%02x%02x)", response[2] == 2 ? "OK" : "NOT_OK", response[2], response[3], - response[4]); + // Check if 5th bit (register CI6) is set + ESP_LOGD(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK", + response[0], response[1], response[2], response[3], response[4], response[5], response[6]); } void SenseAirComponent::abc_enable() { + // Response is just an echo but must be read to clear the buffer ESP_LOGD(TAG, "SenseAir Enabling automatic baseline calibration"); - this->senseair_write_command_(SENSEAIR_COMMAND_ABC_ENABLE, nullptr, 0); + uint8_t command_length = sizeof(SENSEAIR_COMMAND_ABC_ENABLE) / sizeof(SENSEAIR_COMMAND_ABC_ENABLE[0]); + uint8_t response[command_length]; + this->senseair_write_command_(SENSEAIR_COMMAND_ABC_ENABLE, response, command_length); } void SenseAirComponent::abc_disable() { + // Response is just an echo but must be read to clear the buffer ESP_LOGD(TAG, "SenseAir Disabling automatic baseline calibration"); - this->senseair_write_command_(SENSEAIR_COMMAND_ABC_DISABLE, nullptr, 0); + uint8_t command_length = sizeof(SENSEAIR_COMMAND_ABC_DISABLE) / sizeof(SENSEAIR_COMMAND_ABC_DISABLE[0]); + uint8_t response[command_length]; + this->senseair_write_command_(SENSEAIR_COMMAND_ABC_DISABLE, response, command_length); } void SenseAirComponent::abc_get_period() { diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 2d40e12a09..d423793873 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -6,8 +6,8 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_CO2, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_MOLECULE_CO2, + DEVICE_CLASS_CARBON_DIOXIDE, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -39,11 +39,11 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SenseAirComponent), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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, ), } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 89bde9476a..4b2e9dc019 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,5 +1,4 @@ import math -from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv @@ -16,7 +15,6 @@ from esphome.const import ( CONF_FROM, CONF_ICON, CONF_ID, - CONF_INTERNAL, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -27,47 +25,68 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, - CONF_NAME, CONF_MQTT_ID, CONF_FORCE_UPDATE, - UNIT_EMPTY, - ICON_EMPTY, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, - STATE_CLASS_NONE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ] @@ -76,6 +95,7 @@ StateClasses = sensor_ns.enum("StateClass") STATE_CLASSES = { "": StateClasses.STATE_CLASS_NONE, "measurement": StateClasses.STATE_CLASS_MEASUREMENT, + "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") @@ -87,8 +107,7 @@ def validate_send_first_at(value): send_every = value[CONF_SEND_EVERY] if send_first_at is not None and send_first_at > send_every: raise cv.Invalid( - "send_first_at must be smaller than or equal to send_every! {} <= {}" - "".format(send_first_at, send_every) + f"send_first_at must be smaller than or equal to send_every! {send_first_at} <= {send_every}" ) return value @@ -115,7 +134,7 @@ def validate_datapoint(value): # Base sensor_ns = cg.esphome_ns.namespace("sensor") -Sensor = sensor_ns.class_("Sensor", cg.Nameable) +Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") # Triggers @@ -141,6 +160,7 @@ SlidingWindowMovingAverageFilter = sensor_ns.class_( ExponentialMovingAverageFilter = sensor_ns.class_( "ExponentialMovingAverageFilter", Filter ) +ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) @@ -154,20 +174,22 @@ CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter) SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) -unit_of_measurement = cv.string_strict -accuracy_decimals = cv.int_ -icon = cv.icon -device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +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.MQTT_COMPONENT_SCHEMA.extend( +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): unit_of_measurement, - cv.Optional(CONF_ICON): icon, - cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, - cv.Optional(CONF_DEVICE_CLASS): device_class, + 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"), @@ -195,40 +217,46 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( } ) +_UNDEF = object() + def sensor_schema( - unit_of_measurement_: str, - icon_: str, - accuracy_decimals_: int, - device_class_: Optional[str] = DEVICE_CLASS_EMPTY, - state_class_: Optional[str] = STATE_CLASS_NONE, + unit_of_measurement: str = _UNDEF, + icon: str = _UNDEF, + accuracy_decimals: int = _UNDEF, + device_class: str = _UNDEF, + state_class: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA - if unit_of_measurement_ != UNIT_EMPTY: + if unit_of_measurement is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement_ - ): unit_of_measurement + CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement + ): validate_unit_of_measurement } ) - if icon_ != ICON_EMPTY: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon_): icon}) - if accuracy_decimals_ != 0: + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): validate_icon}) + if accuracy_decimals is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_ACCURACY_DECIMALS, default=accuracy_decimals_ - ): accuracy_decimals, + CONF_ACCURACY_DECIMALS, default=accuracy_decimals + ): validate_accuracy_decimals, } ) - if device_class_ != DEVICE_CLASS_EMPTY: + if device_class is not _UNDEF: schema = schema.extend( - {cv.Optional(CONF_DEVICE_CLASS, default=device_class_): device_class} + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } ) - if state_class_ != STATE_CLASS_NONE: + if state_class is not _UNDEF: schema = schema.extend( - {cv.Optional(CONF_STATE_CLASS, default=state_class_): validate_state_class} + {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} ) return schema @@ -354,6 +382,15 @@ async def exponential_moving_average_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY]) +@FILTER_REGISTRY.register( + "throttle_average", ThrottleAverageFilter, cv.positive_time_period_milliseconds +) +async def throttle_average_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config) + await cg.register_component(var, {}) + return var + + @FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( @@ -428,8 +465,7 @@ CONF_DEGREE = "degree" def validate_calibrate_polynomial(config): if config[CONF_DEGREE] >= len(config[CONF_DATAPOINTS]): raise cv.Invalid( - "Degree is too high! Maximum possible degree with given datapoints is " - "{}".format(len(config[CONF_DATAPOINTS]) - 1), + f"Degree is too high! Maximum possible degree with given datapoints is {len(config[CONF_DATAPOINTS]) - 1}", [CONF_DEGREE], ) return config @@ -466,17 +502,14 @@ async def build_filters(config): async def setup_sensor_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) + 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_ICON in config: - cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index c70fb93963..8cd0adbeb2 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -40,7 +40,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences.make_preference(this->parent_->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->parent_->get_object_id_hash()); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; @@ -52,18 +52,18 @@ class ValueRangeTrigger : public Trigger, public Component { protected: void on_state_(float state) { - if (isnan(state)) + if (std::isnan(state)) return; float local_min = this->min_.value(state); float local_max = this->max_.value(state); bool in_range; - if (isnan(local_min) && isnan(local_max)) { + if (std::isnan(local_min) && std::isnan(local_max)) { in_range = this->previous_in_range_; - } else if (isnan(local_min)) { + } else if (std::isnan(local_min)) { in_range = state <= local_max; - } else if (isnan(local_max)) { + } else if (std::isnan(local_max)) { in_range = state >= local_min; } else { in_range = local_min <= state && state <= local_max; @@ -92,9 +92,9 @@ template class SensorInRangeCondition : public Condition void set_max(float max) { this->max_ = max; } bool check(Ts... x) override { const float state = this->parent_->state; - if (isnan(this->min_)) { + if (std::isnan(this->min_)) { return state <= this->max_; - } else if (isnan(this->max_)) { + } else if (std::isnan(this->max_)) { return state >= this->min_; } else { return this->min_ <= state && state <= this->max_; diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index a2058f7d90..321e3a4a4f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,6 +1,7 @@ #include "filter.h" #include "sensor.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace sensor { @@ -8,7 +9,6 @@ namespace sensor { static const char *const TAG = "sensor.filter"; // Filter -uint32_t Filter::expected_interval(uint32_t input) { return input; } void Filter::input(float value) { ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); optional out = this->new_value(value); @@ -29,15 +29,6 @@ void Filter::initialize(Sensor *parent, Filter *next) { this->parent_ = parent; this->next_ = next; } -uint32_t Filter::calculate_remaining_interval(uint32_t input) { - uint32_t this_interval = this->expected_interval(input); - ESP_LOGVV(TAG, "Filter(%p)::calculate_remaining_interval(%u) -> %u", this, input, this_interval); - if (this->next_ == nullptr) { - return this_interval; - } else { - return this->next_->calculate_remaining_interval(this_interval); - } -} // MedianFilter MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at) @@ -45,7 +36,7 @@ MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_fi void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MedianFilter::new_value(float value) { - if (!isnan(value)) { + if (!std::isnan(value)) { while (this->queue_.size() >= this->window_size_) { this->queue_.pop_front(); } @@ -75,15 +66,13 @@ optional MedianFilter::new_value(float value) { return {}; } -uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; } - // MinFilter MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at) : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MinFilter::new_value(float value) { - if (!isnan(value)) { + if (!std::isnan(value)) { while (this->queue_.size() >= this->window_size_) { this->queue_.pop_front(); } @@ -106,15 +95,13 @@ optional MinFilter::new_value(float value) { return {}; } -uint32_t MinFilter::expected_interval(uint32_t input) { return input * this->send_every_; } - // MaxFilter MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at) : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MaxFilter::new_value(float value) { - if (!isnan(value)) { + if (!std::isnan(value)) { while (this->queue_.size() >= this->window_size_) { this->queue_.pop_front(); } @@ -137,8 +124,6 @@ optional MaxFilter::new_value(float value) { return {}; } -uint32_t MaxFilter::expected_interval(uint32_t input) { return input * this->send_every_; } - // SlidingWindowMovingAverageFilter SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at) @@ -146,7 +131,7 @@ SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional SlidingWindowMovingAverageFilter::new_value(float value) { - if (!isnan(value)) { + if (!std::isnan(value)) { if (this->queue_.size() == this->window_size_) { this->sum_ -= this->queue_[0]; this->queue_.pop_front(); @@ -177,13 +162,11 @@ optional SlidingWindowMovingAverageFilter::new_value(float value) { return {}; } -uint32_t SlidingWindowMovingAverageFilter::expected_interval(uint32_t input) { return input * this->send_every_; } - // ExponentialMovingAverageFilter ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every) : send_every_(send_every), send_at_(send_every - 1), alpha_(alpha) {} optional ExponentialMovingAverageFilter::new_value(float value) { - if (!isnan(value)) { + if (!std::isnan(value)) { if (this->first_value_) this->accumulator_ = value; else @@ -203,7 +186,31 @@ optional ExponentialMovingAverageFilter::new_value(float value) { } void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; } -uint32_t ExponentialMovingAverageFilter::expected_interval(uint32_t input) { return input * this->send_every_; } + +// ThrottleAverageFilter +ThrottleAverageFilter::ThrottleAverageFilter(uint32_t time_period) : 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)) { + this->sum_ += value; + this->n_++; + } + return {}; +} +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); + } else { + this->output(this->sum_ / this->n_); + this->sum_ = 0.0f; + this->n_ = 0; + } + }); +} +float ThrottleAverageFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // LambdaFilter LambdaFilter::LambdaFilter(lambda_filter_t lambda_filter) : lambda_filter_(std::move(lambda_filter)) {} @@ -230,14 +237,14 @@ optional MultiplyFilter::new_value(float value) { return value * this->mu FilterOutValueFilter::FilterOutValueFilter(float value_to_filter_out) : value_to_filter_out_(value_to_filter_out) {} optional FilterOutValueFilter::new_value(float value) { - if (isnan(this->value_to_filter_out_)) { - if (isnan(value)) + if (std::isnan(this->value_to_filter_out_)) { + if (std::isnan(value)) return {}; else return value; } else { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = pow10f(accuracy); + float accuracy_mult = powf(10.0f, accuracy); float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); float rounded_value = roundf(accuracy_mult * value); if (rounded_filter_out == rounded_value) @@ -262,9 +269,9 @@ optional ThrottleFilter::new_value(float value) { // DeltaFilter DeltaFilter::DeltaFilter(float min_delta) : min_delta_(min_delta), last_value_(NAN) {} optional DeltaFilter::new_value(float value) { - if (isnan(value)) + if (std::isnan(value)) return {}; - if (isnan(this->last_value_)) { + if (std::isnan(this->last_value_)) { return this->last_value_ = value; } if (fabsf(value - this->last_value_) >= this->min_delta_) { @@ -296,14 +303,6 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } -uint32_t OrFilter::expected_interval(uint32_t input) { - uint32_t min_interval = UINT32_MAX; - for (Filter *filter : this->filters_) { - min_interval = std::min(min_interval, filter->calculate_remaining_interval(input)); - } - - return min_interval; -} // DebounceFilter optional DebounceFilter::new_value(float value) { this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); @@ -324,7 +323,6 @@ optional HeartbeatFilter::new_value(float value) { return {}; } -uint32_t HeartbeatFilter::expected_interval(uint32_t input) { return this->time_period_; } void HeartbeatFilter::setup() { this->set_interval("heartbeat", this->time_period_, [this]() { ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 270a91ccff..d595e419a6 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -33,11 +33,6 @@ class Filter { void input(float value); - /// Return the amount of time that this filter is expected to take based on the input time interval. - virtual uint32_t expected_interval(uint32_t input); - - uint32_t calculate_remaining_interval(uint32_t input); - void output(float value); protected: @@ -68,8 +63,6 @@ class MedianFilter : public Filter { void set_send_every(size_t send_every); void set_window_size(size_t window_size); - uint32_t expected_interval(uint32_t input) override; - protected: std::deque queue_; size_t send_every_; @@ -98,8 +91,6 @@ class MinFilter : public Filter { void set_send_every(size_t send_every); void set_window_size(size_t window_size); - uint32_t expected_interval(uint32_t input) override; - protected: std::deque queue_; size_t send_every_; @@ -128,8 +119,6 @@ class MaxFilter : public Filter { void set_send_every(size_t send_every); void set_window_size(size_t window_size); - uint32_t expected_interval(uint32_t input) override; - protected: std::deque queue_; size_t send_every_; @@ -159,8 +148,6 @@ class SlidingWindowMovingAverageFilter : public Filter { void set_send_every(size_t send_every); void set_window_size(size_t window_size); - uint32_t expected_interval(uint32_t input) override; - protected: float sum_{0.0}; std::deque queue_; @@ -183,8 +170,6 @@ class ExponentialMovingAverageFilter : public Filter { void set_send_every(size_t send_every); void set_alpha(float alpha); - uint32_t expected_interval(uint32_t input) override; - protected: bool first_value_{true}; float accumulator_{0.0f}; @@ -193,6 +178,26 @@ class ExponentialMovingAverageFilter : public Filter { float alpha_; }; +/** Simple throttle average filter. + * + * It takes the average of all the values received in a period of time. + */ +class ThrottleAverageFilter : public Filter, public Component { + public: + explicit ThrottleAverageFilter(uint32_t time_period); + + void setup() override; + + optional new_value(float value) override; + + float get_setup_priority() const override; + + protected: + uint32_t time_period_; + float sum_{0.0f}; + unsigned int n_{0}; +}; + using lambda_filter_t = std::function(float)>; /** This class allows for creation of simple template filters. @@ -279,8 +284,6 @@ class HeartbeatFilter : public Filter, public Component { optional new_value(float value) override; - uint32_t expected_interval(uint32_t input) override; - float get_setup_priority() const override; protected: @@ -306,8 +309,6 @@ class OrFilter : public Filter { void initialize(Sensor *parent, Filter *next) override; - uint32_t expected_interval(uint32_t input) override; - optional new_value(float value) override; protected: diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index b19d8be634..793ae170c3 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -6,16 +6,55 @@ namespace sensor { static const char *const TAG = "sensor"; -const char *state_class_to_string(StateClass state_class) { +std::string state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: return "measurement"; + case STATE_CLASS_TOTAL_INCREASING: + return "total_increasing"; case STATE_CLASS_NONE: default: return ""; } } +Sensor::Sensor(const std::string &name) : EntityBase(name), state(NAN), raw_state(NAN) {} +Sensor::Sensor() : Sensor("") {} + +std::string Sensor::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return this->unit_of_measurement(); +} +void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} +std::string Sensor::unit_of_measurement() { return ""; } + +int8_t Sensor::get_accuracy_decimals() { + if (this->accuracy_decimals_.has_value()) + return *this->accuracy_decimals_; + return this->accuracy_decimals(); +} +void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } +int8_t Sensor::accuracy_decimals() { return 0; } + +std::string Sensor::get_device_class() { + if (this->device_class_.has_value()) + return *this->device_class_; + return this->device_class(); +} +void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } +std::string Sensor::device_class() { return ""; } + +void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } +StateClass Sensor::get_state_class() { + if (this->state_class_.has_value()) + return *this->state_class_; + return this->state_class(); +} +StateClass Sensor::state_class() { return StateClass::STATE_CLASS_NONE; } + void Sensor::publish_state(float state) { this->raw_state = state; this->raw_callback_.call(state); @@ -28,53 +67,12 @@ void Sensor::publish_state(float state) { this->filter_list_->input(state); } } -void Sensor::push_new_value(float state) { this->publish_state(state); } -std::string Sensor::unit_of_measurement() { return ""; } -std::string Sensor::icon() { return ""; } -uint32_t Sensor::update_interval() { return 0; } -int8_t Sensor::accuracy_decimals() { return 0; } -Sensor::Sensor(const std::string &name) : Nameable(name), state(NAN), raw_state(NAN) {} -Sensor::Sensor() : Sensor("") {} -void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} -void Sensor::set_icon(const std::string &icon) { this->icon_ = icon; } -void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { this->raw_callback_.add(std::move(callback)); } -std::string Sensor::get_icon() { - if (this->icon_.has_value()) - return *this->icon_; - return this->icon(); -} -void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } -std::string Sensor::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return this->device_class(); -} -std::string Sensor::device_class() { return ""; } -void Sensor::set_state_class(StateClass state_class) { this->state_class = state_class; } -void Sensor::set_state_class(const std::string &state_class) { - if (str_equals_case_insensitive(state_class, "measurement")) { - this->state_class = STATE_CLASS_MEASUREMENT; - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str()); - } -} -std::string Sensor::get_unit_of_measurement() { - if (this->unit_of_measurement_.has_value()) - return *this->unit_of_measurement_; - return this->unit_of_measurement(); -} -int8_t Sensor::get_accuracy_decimals() { - if (this->accuracy_decimals_.has_value()) - return *this->accuracy_decimals_; - return this->accuracy_decimals(); -} + void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -104,9 +102,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } -float Sensor::get_value() const { return this->state; } float Sensor::get_state() const { return this->state; } -float Sensor::get_raw_value() const { return this->raw_state; } float Sensor::get_raw_state() const { return this->raw_state; } std::string Sensor::unique_id() { return ""; } @@ -118,24 +114,7 @@ void Sensor::internal_send_state_to_frontend(float state) { this->callback_.call(state); } bool Sensor::has_state() const { return this->has_state_; } -uint32_t Sensor::calculate_expected_filter_update_interval() { - uint32_t interval = this->update_interval(); - if (interval == 4294967295UL) - // update_interval: never - return 0; - - if (this->filter_list_ == nullptr) { - return interval; - } - - return this->filter_list_->calculate_remaining_interval(interval); -} uint32_t Sensor::hash_base() { return 2455723294UL; } -PollingSensorComponent::PollingSensorComponent(const std::string &name, uint32_t update_interval) - : PollingComponent(update_interval), Sensor(name) {} - -uint32_t PollingSensorComponent::update_interval() { return this->get_update_interval(); } - } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 9b1f4d7f86..6cab46f7f9 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -1,6 +1,8 @@ #pragma once +#include "esphome/core/log.h" #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/components/sensor/filter.h" @@ -9,11 +11,11 @@ namespace sensor { #define LOG_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + 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()); \ } \ - ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->state_class)); \ + ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->get_state_class()).c_str()); \ ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->get_unit_of_measurement().c_str()); \ ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, (obj)->get_accuracy_decimals()); \ if (!(obj)->get_icon().empty()) { \ @@ -33,39 +35,51 @@ namespace sensor { enum StateClass : uint8_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, + STATE_CLASS_TOTAL_INCREASING = 2, }; -const char *state_class_to_string(StateClass state_class); +std::string state_class_to_string(StateClass state_class); /** Base-class for all sensors. * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. */ -class Sensor : public Nameable { +class Sensor : public EntityBase { public: explicit Sensor(); explicit Sensor(const std::string &name); - /** Manually set the unit of measurement of this sensor. By default the sensor's default defined by - * unit_of_measurement() is used. - * - * @param unit_of_measurement The unit of measurement, "" to disable. - */ + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + /// Manually set the unit of measurement. void set_unit_of_measurement(const std::string &unit_of_measurement); - /** Manually set the icon of this sensor. By default the sensor's default defined by icon() is used. - * - * @param icon The icon, for example "mdi:flash". "" to disable. - */ - void set_icon(const std::string &icon); - - /** Manually set the accuracy in decimals for this sensor. By default, the sensor's default defined by - * accuracy_decimals() is used. - * - * @param accuracy_decimals The accuracy decimal that should be used. - */ + /// Get the accuracy in decimals, using the manual override if set. + int8_t get_accuracy_decimals(); + /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); + /// Get the device class, using the manual override if set. + std::string get_device_class(); + /// Manually set the device class. + void set_device_class(const std::string &device_class); + + /// Get the state class, using the manual override if set. + StateClass get_state_class(); + /// Manually set the state class. + void set_state_class(StateClass state_class); + + /** + * Get whether force update mode is enabled. + * + * If the sensor is in force_update mode, the frontend is required to save all + * state changes to the database when they are published, even if the state is the + * same as before. + */ + bool get_force_update() const { return force_update_; } + /// Set force update mode. + void set_force_update(bool force_update) { force_update_ = force_update; } + /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -87,24 +101,11 @@ class Sensor : public Nameable { /// Clear the entire filter chain. void clear_filters(); - /// Getter-syntax for .value. Please use .state instead. - float get_value() const ESPDEPRECATED(".value is deprecated, please use .state"); /// Getter-syntax for .state. float get_state() const; - /// Getter-syntax for .raw_value. Please use .raw_state instead. - float get_raw_value() const ESPDEPRECATED(".raw_value is deprecated, please use .raw_state"); /// Getter-syntax for .raw_state float get_raw_state() const; - /// Get the accuracy in decimals. Uses the manual override if specified or the default value instead. - int8_t get_accuracy_decimals(); - - /// Get the unit of measurement. Uses the manual override if specified or the default value instead. - std::string get_unit_of_measurement(); - - /// Get the Home Assistant Icon. Uses the manual override if specified or the default value instead. - std::string get_icon(); - /** Publish a new state to the front-end. * * First, the new state will be assigned to the raw_value. Then it's passed through all filters @@ -114,12 +115,6 @@ class Sensor : public Nameable { */ void publish_state(float state); - /** Push a new value to the MQTT front-end. - * - * Note: deprecated, please use publish_state. - */ - void push_new_value(float state) ESPDEPRECATED("push_new_value is deprecated. Please use .publish_state instead"); - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Add a callback that will be called every time a filtered value arrives. @@ -136,35 +131,15 @@ class Sensor : public Nameable { */ float state; - /// Manually set the Home Assistant device class (see sensor::device_class) - void set_device_class(const std::string &device_class); - - /// Get the device class for this sensor, using the manual override if specified. - std::string get_device_class(); - - /** This member variable stores the current raw state of the sensor. Unlike .state, - * this will be updated immediately when publish_state is called. + /** This member variable stores the current raw state of the sensor, without any filters applied. + * + * Unlike .state,this will be updated immediately when publish_state is called. */ float raw_state; /// Return whether this sensor has gotten a full state (that passed through all filters) yet. bool has_state() const; - // The state class of this sensor state - StateClass state_class{STATE_CLASS_NONE}; - - /// Manually set the Home Assistant state class (see sensor::state_class) - void set_state_class(StateClass state_class); - void set_state_class(const std::string &state_class); - - /** Override this to set the Home Assistant device class for this sensor. - * - * Return "" to disable this feature. - * - * @return The device class of this sensor, for example "temperature". - */ - virtual std::string device_class(); - /** A unique ID for this sensor, empty for no unique id. See unique ID requirements: * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements * @@ -172,65 +147,34 @@ class Sensor : public Nameable { */ virtual std::string unique_id(); - /// Return with which interval the sensor is polled. Return 0 for non-polling mode. - virtual uint32_t update_interval(); - - /// Calculate the expected update interval for values that pass through all filters. - uint32_t calculate_expected_filter_update_interval(); - void internal_send_state_to_frontend(float state); - bool get_force_update() const { return force_update_; } - /** Set this sensor's force_update mode. - * - * If the sensor is in force_update mode, the frontend is required to save all - * state changes to the database when they are published, even if the state is the - * same as before. - */ - void set_force_update(bool force_update) { force_update_ = force_update; } - protected: - /** Override this to set the Home Assistant unit of measurement for this sensor. - * - * Return "" to disable this feature. - * - * @return The icon of this sensor, for example "°C". - */ + /// Override this to set the default unit of measurement. virtual std::string unit_of_measurement(); // NOLINT - /** Override this to set the Home Assistant icon for this sensor. - * - * Return "" to disable this feature. - * - * @return The icon of this sensor, for example "mdi:battery". - */ - virtual std::string icon(); // NOLINT - - /// Return the accuracy in decimals for this sensor. + /// Override this to set the default accuracy in decimals. virtual int8_t accuracy_decimals(); // NOLINT - optional device_class_{}; ///< Stores the override of the device class + /// Override this to set the default device class. + virtual std::string device_class(); // NOLINT + + /// Override this to set the default state class. + virtual StateClass state_class(); // NOLINT uint32_t hash_base() override; CallbackManager raw_callback_; ///< Storage for raw state callbacks. CallbackManager callback_; ///< Storage for filtered state callbacks. - /// Override the unit of measurement - optional unit_of_measurement_; - /// Override the icon advertised to Home Assistant, otherwise sensor's icon will be used. - optional icon_; - /// Override the accuracy in decimals, otherwise the sensor's values will be used. - optional accuracy_decimals_; - Filter *filter_list_{nullptr}; ///< Store all active filters. + bool has_state_{false}; - bool force_update_{false}; -}; + Filter *filter_list_{nullptr}; ///< Store all active filters. -class PollingSensorComponent : public PollingComponent, public Sensor { - public: - explicit PollingSensorComponent(const std::string &name, uint32_t update_interval); - - uint32_t update_interval() override; + optional unit_of_measurement_; ///< Unit of measurement override + optional accuracy_decimals_; ///< Accuracy in decimals override + optional device_class_; ///< Device class override + optional state_class_{STATE_CLASS_NONE}; ///< State class override + bool force_update_{false}; ///< Force update mode }; } // namespace sensor diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index 0b018ddb2e..2e1ba587a2 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -1,5 +1,6 @@ #include "servo.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace servo { diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index d95a524a8b..e2e3823158 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -24,7 +24,7 @@ class Servo : public Component { void setup() override { float v; if (this->restore_) { - this->rtc_ = global_preferences.make_preference(global_servo_id); + this->rtc_ = global_preferences->make_preference(global_servo_id); global_servo_id++; if (this->rtc_.load(&v)) { this->output_->set_level(v); diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index f393627eda..2596e0065d 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -3,13 +3,16 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, + CONF_BASELINE, + CONF_ECO2, + CONF_TVOC, ICON_RADIATOR, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2, - CONF_TVOC, ) DEPENDENCIES = ["i2c"] @@ -17,10 +20,9 @@ DEPENDENCIES = ["i2c"] sgp30_ns = cg.esphome_ns.namespace("sgp30") SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice) -CONF_ECO2 = "eco2" -CONF_BASELINE = "baseline" CONF_ECO2_BASELINE = "eco2_baseline" CONF_TVOC_BASELINE = "tvoc_baseline" +CONF_STORE_BASELINE = "store_baseline" CONF_UPTIME = "uptime" CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" @@ -31,19 +33,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SGP30Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + ), + cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + ), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_BASELINE): cv.Schema( { cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, @@ -58,7 +69,7 @@ CONFIG_SCHEMA = ( ), } ) - .extend(cv.polling_component_schema("60s")) + .extend(cv.polling_component_schema("1s")) .extend(i2c.i2c_device_schema(0x58)) ) @@ -76,6 +87,17 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_tvoc_sensor(sens)) + if CONF_ECO2_BASELINE in config: + sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE]) + cg.add(var.set_eco2_baseline_sensor(sens)) + + if CONF_TVOC_BASELINE in config: + sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE]) + cg.add(var.set_tvoc_baseline_sensor(sens)) + + if CONF_STORE_BASELINE in config: + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + if CONF_BASELINE in config: baseline_config = config[CONF_BASELINE] cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 56e5c7214c..1a64a12907 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,5 +1,7 @@ #include "sgp30.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" +#include namespace esphome { namespace sgp30 { @@ -22,6 +24,13 @@ const uint32_t IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; // if the sensor starts without any prior baseline value provided const uint32_t IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; +// Shortest time interval of 1H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 3600; + +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + void SGP30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP30..."); @@ -39,7 +48,7 @@ void SGP30Component::setup() { } this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { @@ -73,6 +82,21 @@ void SGP30Component::setup() { return; } + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences->make_preference(hash, true); + + if (this->pref_.load(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + baselines_storage_.tvoc); + this->eco2_baseline_ = this->baselines_storage_.eco2; + this->tvoc_baseline_ = this->baselines_storage_.tvoc; + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + // Sensor baseline reliability timer if (this->eco2_baseline_ > 0 && this->tvoc_baseline_ > 0) { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; @@ -110,6 +134,31 @@ void SGP30Component::read_iaq_baseline_() { uint16_t tvocbaseline = (raw_data[1]); ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); + if (eco2baseline != this->eco2_baseline_ || tvocbaseline != this->tvoc_baseline_) { + this->eco2_baseline_ = eco2baseline; + this->tvoc_baseline_ = tvocbaseline; + if (this->eco2_sensor_baseline_ != nullptr) + this->eco2_sensor_baseline_->publish_state(this->eco2_baseline_); + if (this->tvoc_sensor_baseline_ != nullptr) + this->tvoc_sensor_baseline_->publish_state(this->tvoc_baseline_); + + // 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 || + abs(this->baselines_storage_.eco2 - this->eco2_baseline_) > MAXIMUM_STORAGE_DIFF || + abs(this->baselines_storage_.tvoc - this->tvoc_baseline_) > MAXIMUM_STORAGE_DIFF)) { + this->seconds_since_last_store_ = 0; + this->baselines_storage_.eco2 = this->eco2_baseline_; + this->baselines_storage_.tvoc = this->tvoc_baseline_; + if (this->pref_.save(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Store eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + this->baselines_storage_.tvoc); + } else { + ESP_LOGW(TAG, "Could not store eCO2 and TVOC baselines"); + } + } + } this->status_clear_warning(); }); } else { @@ -124,7 +173,7 @@ void SGP30Component::send_env_data_() { float humidity = NAN; if (this->humidity_sensor_ != nullptr) humidity = this->humidity_sensor_->state; - if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); return; } else { @@ -134,7 +183,7 @@ void SGP30Component::send_env_data_() { if (this->temperature_sensor_ != nullptr) { temperature = float(this->temperature_sensor_->state); } - if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); return; } else { @@ -171,7 +220,8 @@ void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_b if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); } else - ESP_LOGI(TAG, "Initial eCO2 and TVOC baselines applied successfully!"); + ESP_LOGI(TAG, "Initial baselines applied successfully! eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, + tvoc_baseline); } void SGP30Component::dump_config() { @@ -196,7 +246,7 @@ void SGP30Component::dump_config() { break; } } else { - ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { ESP_LOGCONFIG(TAG, " Baseline:"); ESP_LOGCONFIG(TAG, " eCO2 Baseline: 0x%04X", this->eco2_baseline_); @@ -207,8 +257,11 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, " Warm up time: %us", this->required_warm_up_time_); } LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "eCO2", this->eco2_sensor_); - LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "eCO2 sensor", this->eco2_sensor_); + LOG_SENSOR(" ", "TVOC sensor", this->tvoc_sensor_); + LOG_SENSOR(" ", "eCO2 baseline sensor", this->eco2_sensor_baseline_); + LOG_SENSOR(" ", "TVOC baseline sensor", this->tvoc_sensor_baseline_); + ESP_LOGCONFIG(TAG, "Store baseline: %s", YESNO(this->store_baseline_)); if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); @@ -223,7 +276,7 @@ void SGP30Component::update() { this->status_set_warning(); return; } - + this->seconds_since_last_store_ += this->update_interval_ / 1000; this->set_timeout(50, [this]() { uint16_t raw_data[2]; if (!this->read_data_(raw_data, 2)) { @@ -239,6 +292,11 @@ void SGP30Component::update() { this->eco2_sensor_->publish_state(eco2); if (this->tvoc_sensor_ != nullptr) this->tvoc_sensor_->publish_state(tvoc); + + if (this->get_update_interval() != 1000) { + ESP_LOGW(TAG, "Update interval for SGP30 sensor must be set to 1s for optimized readout"); + } + this->status_clear_warning(); this->send_env_data_(); this->read_iaq_baseline_(); @@ -275,10 +333,9 @@ uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) { bool SGP30Component::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -287,13 +344,11 @@ bool SGP30Component::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sht_crc_(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 298a78e8dd..91a1c1e9c7 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -3,16 +3,25 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/preferences.h" #include namespace esphome { namespace sgp30 { +struct SGP30Baselines { + uint16_t eco2; + uint16_t tvoc; +} PACKED; + /// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. class SGP30Component : public PollingComponent, public i2c::I2CDevice { public: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_eco2_baseline_sensor(sensor::Sensor *eco2_baseline) { eco2_sensor_baseline_ = eco2_baseline; } + void set_tvoc_baseline_sensor(sensor::Sensor *tvoc_baseline) { tvoc_sensor_baseline_ = tvoc_baseline; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_eco2_baseline(uint16_t eco2_baseline) { eco2_baseline_ = eco2_baseline; } void set_tvoc_baseline(uint16_t tvoc_baseline) { tvoc_baseline_ = tvoc_baseline; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } @@ -34,6 +43,9 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { uint64_t serial_number_; uint16_t featureset_; uint32_t required_warm_up_time_; + uint32_t seconds_since_last_store_; + SGP30Baselines baselines_storage_; + ESPPreferenceObject pref_; enum ErrorCode { COMMUNICATION_FAILED, @@ -45,8 +57,12 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *eco2_sensor_baseline_{nullptr}; + sensor::Sensor *tvoc_sensor_baseline_{nullptr}; uint16_t eco2_baseline_{0x0000}; uint16_t tvoc_baseline_{0x0000}; + bool store_baseline_; + /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 36e039d2b5..7b96f867af 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -3,10 +3,9 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) DEPENDENCIES = ["i2c"] @@ -26,7 +25,10 @@ CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 3b634353c4..a3d2c74eb7 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -1,5 +1,7 @@ -#include "esphome/core/log.h" #include "sgp40.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include namespace esphome { namespace sgp40 { @@ -23,7 +25,7 @@ void SGP40Component::setup() { } this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { @@ -54,7 +56,7 @@ void SGP40Component::setup() { // Hash with compilation time // This ensures the baseline storage is cleared after OTA uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences.make_preference(hash, true); + this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->baselines_storage_)) { this->state0_ = this->baselines_storage_.state0; @@ -75,30 +77,45 @@ void SGP40Component::setup() { } this->self_test_(); + + /* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf + indicates this sensor should be driven at 1Hz. Comments from the developers at: + https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit + resilient to slight timing variations so the software timer should be accurate enough for + this. + + This block starts sampling from the sensor at 1Hz, and is done seperately from the call + to the update method. This seperation is to support getting accurate measurements but + limit the amount of communication done over wifi for power consumption or to keep the + 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_voc_index(); }); } void SGP40Component::self_test_() { - ESP_LOGD(TAG, "selfTest started"); + ESP_LOGD(TAG, "Self-test started"); if (!this->write_command_(SGP40_CMD_SELF_TEST)) { this->error_code_ = COMMUNICATION_FAILED; - ESP_LOGD(TAG, "selfTest communicatin failed"); + ESP_LOGD(TAG, "Self-test communication failed"); this->mark_failed(); } this->set_timeout(250, [this]() { uint16_t reply[1]; if (!this->read_data_(reply, 1)) { - ESP_LOGD(TAG, "selfTest read_data_ failed"); + ESP_LOGD(TAG, "Self-test read_data_ failed"); this->mark_failed(); return; } if (reply[0] == 0xD400) { - ESP_LOGD(TAG, "selfTest completed"); + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); return; } - ESP_LOGD(TAG, "selfTest failed"); + ESP_LOGD(TAG, "Self-test failed"); this->mark_failed(); }); } @@ -154,10 +171,16 @@ int32_t SGP40Component::measure_voc_index_() { */ uint16_t SGP40Component::measure_raw_() { float humidity = NAN; + + if (!this->self_test_complete_) { + ESP_LOGD(TAG, "Self-test not yet complete"); + return UINT16_MAX; + } + if (this->humidity_sensor_ != nullptr) { humidity = this->humidity_sensor_->state; } - if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { humidity = 50; } @@ -165,7 +188,7 @@ uint16_t SGP40Component::measure_raw_() { if (this->temperature_sensor_ != nullptr) { temperature = float(this->temperature_sensor_->state); } - if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { temperature = 25; } @@ -183,9 +206,9 @@ uint16_t SGP40Component::measure_raw_() { command[6] = tempticks & 0xFF; command[7] = generate_crc_(command + 5, 2); - if (!this->write_bytes_raw(command, 8)) { + if (this->write(command, 8) != i2c::ERROR_OK) { this->status_set_warning(); - ESP_LOGD(TAG, "write_bytes_raw error"); + ESP_LOGD(TAG, "write error"); return UINT16_MAX; } delay(250); // NOLINT @@ -215,21 +238,26 @@ uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) { return crc; } -void SGP40Component::update() { - this->seconds_since_last_store_ += this->update_interval_ / 1000; - - uint32_t voc_index = this->measure_voc_index_(); +void SGP40Component::update_voc_index() { + this->seconds_since_last_store_ += 1; + this->voc_index_ = this->measure_voc_index_(); if (this->samples_read_ < this->samples_to_stabalize_) { 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_stabalize_, voc_index); + this->samples_to_stabalize_, this->voc_index_); + return; + } +} + +void SGP40Component::update() { + if (this->samples_read_ < this->samples_to_stabalize_) { return; } - if (voc_index != UINT16_MAX) { + if (this->voc_index_ != UINT16_MAX) { this->status_clear_warning(); - this->publish_state(voc_index); + this->publish_state(this->voc_index_); } else { this->status_set_warning(); } @@ -238,6 +266,8 @@ void SGP40Component::update() { void SGP40Component::dump_config() { ESP_LOGCONFIG(TAG, "SGP40:"); LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_); + if (this->is_failed()) { switch (this->error_code_) { case COMMUNICATION_FAILED: @@ -248,7 +278,7 @@ void SGP40Component::dump_config() { break; } } else { - ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); } LOG_UPDATE_INTERVAL(this); @@ -294,7 +324,7 @@ bool SGP40Component::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf.data(), num_bytes)) { + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index b9ea365169..bb68a1ffcf 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -46,6 +46,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 void setup() override; void update() override; + void update_voc_index(); 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; } @@ -68,9 +69,11 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 int32_t seconds_since_last_store_; SGP40Baselines baselines_storage_; VocAlgorithmParams voc_algorithm_params_; + bool self_test_complete_; bool store_baseline_; int32_t state0_; int32_t state1_; + int32_t voc_index_ = 0; uint8_t samples_read_ = 0; uint8_t samples_to_stabalize_ = static_cast(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2; diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index 2a4bb6594e..b9e7bce733 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT3XDComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 0bf2cc1a81..56a43d5161 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -101,10 +101,9 @@ uint8_t sht_crc(uint8_t data1, uint8_t data2) { bool SHT3XDComponent::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -113,13 +112,11 @@ bool SHT3XDComponent::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sht_crc(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index a746ecde07..a66ca1a526 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -51,18 +51,18 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT4XComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 6d7c917b57..248f32c4de 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -12,7 +12,7 @@ void SHT4XComponent::start_heater_() { uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; ESP_LOGD(TAG, "Heater turning on"); - this->write_bytes_raw(cmd, 1); + this->write(cmd, 1); } void SHT4XComponent::setup() { @@ -53,7 +53,7 @@ void SHT4XComponent::update() { uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; // Send command - this->write_bytes_raw(cmd, 1); + this->write(cmd, 1); this->set_timeout(10, [this]() { const uint8_t num_bytes = 6; diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index af9379218c..ba2283a9b4 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHTCXComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 848da8d7d7..f2fb6bd5c3 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -1,5 +1,6 @@ #include "shtcx.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace shtcx { @@ -130,10 +131,9 @@ uint8_t sht_crc(uint8_t data1, uint8_t data2) { bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -142,13 +142,11 @@ bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sht_crc(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/shutdown_switch.cpp index 3cc8ba9e1b..a5f9a92982 100644 --- a/esphome/components/shutdown/shutdown_switch.cpp +++ b/esphome/components/shutdown/shutdown_switch.cpp @@ -1,7 +1,15 @@ #include "shutdown_switch.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#ifdef USE_ESP32 +#include +#endif +#ifdef USE_ESP8266 +#include +#endif + namespace esphome { namespace shutdown { @@ -17,10 +25,10 @@ void ShutdownSwitch::write_state(bool state) { delay(100); // NOLINT App.run_safe_shutdown_hooks(); -#ifdef ARDUINO_ARCH_ESP8266 - ESP.deepSleep(0); +#ifdef USE_ESP8266 + ESP.deepSleep(0); // NOLINT(readability-static-accessed-through-instance) #endif -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 esp_deep_sleep_start(); #endif } diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 40c011a769..0887b8640f 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -40,6 +40,9 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("5s")) .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "sim800l", baud_rate=9600, require_tx=True, require_rx=True +) async def to_code(config): @@ -54,10 +57,6 @@ async def to_code(config): ) -def validate(config, item_config): - uart.validate_device("sim800l", config, item_config, baud_rate=9600) - - SIM800L_SEND_SMS_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(Sim800LComponent), diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index fa9c392bfc..21e9ac4a50 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -74,7 +74,7 @@ class Sim800LReceivedMessageTrigger : public Trigger { public: explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { parent->add_on_sms_received_callback( - [this](std::string message, std::string sender) { this->trigger(std::move(message), std::move(sender)); }); + [this](const std::string &message, const std::string &sender) { this->trigger(message, sender); }); } }; diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index 4a2c1d0a14..f0524f36d8 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" namespace esphome { diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h index 85f78c8cf5..73414c0003 100644 --- a/esphome/components/sm16716/sm16716.h +++ b/esphome/components/sm16716/sm16716.h @@ -1,8 +1,9 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" +#include namespace esphome { namespace sm16716 { diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h index e39730579f..0277e9ba1c 100644 --- a/esphome/components/sm2135/sm2135.h +++ b/esphome/components/sm2135/sm2135.h @@ -1,8 +1,9 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" +#include namespace esphome { namespace sm2135 { diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py index 3d522b3bd5..0c3c54f200 100644 --- a/esphome/components/sm300d2/sensor.py +++ b/esphome/components/sm300d2/sensor.py @@ -10,7 +10,10 @@ from esphome.const import ( CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, @@ -18,7 +21,6 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, UNIT_PERCENT, - ICON_EMPTY, ICON_MOLECULE_CO2, ICON_FLASK, ICON_CHEMICAL_WEAPON, @@ -35,53 +37,50 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SM300D2Sensor), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_FLASK, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TVOC): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp index 34d80349f9..e41a4855db 100644 --- a/esphome/components/sm300d2/sm300d2.cpp +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -9,10 +9,12 @@ static const uint8_t SM300D2_RESPONSE_LENGTH = 17; void SM300D2Sensor::update() { uint8_t response[SM300D2_RESPONSE_LENGTH]; + uint8_t peeked; + + while (this->available() > 0 && this->peek_byte(&peeked) && peeked != 0x3C) + this->read(); - flush(); bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH); - flush(); if (!read_success) { ESP_LOGW(TAG, "Reading data from SM300D2 failed!"); @@ -27,7 +29,11 @@ void SM300D2Sensor::update() { } uint16_t calculated_checksum = this->sm300d2_checksum_(response); - if (calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) { + // Occasionally the checksum has a +/- 0x80 offset. Negative temperatures are + // responsible for some of these. The rest are unknown/undocumented. + if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) && + (calculated_checksum - 0x80 != response[SM300D2_RESPONSE_LENGTH - 1]) && + (calculated_checksum + 0x80 != response[SM300D2_RESPONSE_LENGTH - 1])) { ESP_LOGW(TAG, "SM300D2 Checksum doesn't match: 0x%02X!=0x%02X", response[SM300D2_RESPONSE_LENGTH - 1], calculated_checksum); this->status_set_warning(); @@ -43,7 +49,10 @@ void SM300D2Sensor::update() { const uint16_t tvoc = (response[6] * 256) + response[7]; const uint16_t pm_2_5 = (response[8] * 256) + response[9]; const uint16_t pm_10_0 = (response[10] * 256) + response[11]; - const float temperature = response[12] + (response[13] * 0.1); + // A negative value is indicated by adding 0x80 (128) to the temperature value + const float temperature = ((response[12] + (response[13] * 0.1)) > 128) + ? (((response[12] + (response[13] * 0.1)) - 128) * -1) + : response[12] + (response[13] * 0.1); const float humidity = response[14] + (response[15] * 0.1); ESP_LOGD(TAG, "Received CO₂: %u ppm", co2); @@ -62,7 +71,7 @@ void SM300D2Sensor::update() { if (this->pm_2_5_sensor_ != nullptr) this->pm_2_5_sensor_->publish_state(pm_2_5); - ESP_LOGD(TAG, "Received pm_10_0: %u µg/m³", pm_10_0); + ESP_LOGD(TAG, "Received PM10: %u µg/m³", pm_10_0); if (this->pm_10_0_sensor_ != nullptr) this->pm_10_0_sensor_->publish_state(pm_10_0); diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 4437878970..0d1ff6ecba 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -3,10 +3,12 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import ( CONF_ID, + CONF_MODE, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, CONF_CLOCK_PIN, + CONF_OUTPUT, ) DEPENDENCIES = [] @@ -48,19 +50,36 @@ async def to_code(config): cg.add(var.set_sr_count(config[CONF_SR_COUNT])) -SN74HC595_OUTPUT_PIN_SCHEMA = cv.Schema( +def _validate_output_mode(value): + if value is not True: + raise cv.Invalid("Only output mode is supported") + return value + + +SN74HC595_PIN_SCHEMA = cv.All( { + cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_, + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + 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, } ) -SN74HC595_INPUT_PIN_SCHEMA = cv.Schema({}) -@pins.PIN_SCHEMA_REGISTRY.register( - CONF_SN74HC595, (SN74HC595_OUTPUT_PIN_SCHEMA, SN74HC595_INPUT_PIN_SCHEMA) -) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, SN74HC595_PIN_SCHEMA) async def sn74hc595_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_SN74HC595]) - return SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED]) + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + return var diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index 596ebf755d..5ebf50e5cb 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -10,17 +10,17 @@ void SN74HC595Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); if (this->have_oe_pin_) { // disable output - this->oe_pin_->pin_mode(OUTPUT); + this->oe_pin_->setup(); this->oe_pin_->digital_write(true); } // initialize output pins - this->clock_pin_->pin_mode(OUTPUT); - this->data_pin_->pin_mode(OUTPUT); - this->latch_pin_->pin_mode(OUTPUT); - this->clock_pin_->digital_write(LOW); - this->data_pin_->digital_write(LOW); - this->latch_pin_->digital_write(LOW); + this->clock_pin_->setup(); + this->data_pin_->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_(); @@ -62,16 +62,14 @@ bool SN74HC595Component::write_gpio_() { float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } -void SN74HC595GPIOPin::setup() {} - -bool SN74HC595GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } - void SN74HC595GPIOPin::digital_write(bool value) { this->parent_->digital_write_(this->pin_, value != this->inverted_); } - -SN74HC595GPIOPin::SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted) - : GPIOPin(pin, OUTPUT, inverted), parent_(parent) {} +std::string SN74HC595GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via SN74HC595", pin_); + return buffer; +} } // namespace sn74hc595 } // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index d6f9a68bc8..784019c3a6 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace sn74hc595 { @@ -41,14 +41,20 @@ class SN74HC595Component : public Component { /// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. class SN74HC595GPIOPin : public GPIOPin { public: - SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted = false); - - void setup() override; - bool digital_read() override; + 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; + + void set_parent(SN74HC595Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } protected: SN74HC595Component *parent_; + uint8_t pin_; + bool inverted_; }; } // namespace sn74hc595 diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index b667f3b1ce..2b6cd10e80 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -1,13 +1,18 @@ #include "sntp_component.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include "lwip/apps/sntp.h" #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include "sntp.h" #endif +// Yes, the server names are leaked, but that's fine. +#ifdef CLANG_TIDY +#define strdup(x) (const_cast(x)) +#endif + namespace esphome { namespace sntp { @@ -15,13 +20,13 @@ static const char *const TAG = "sntp"; void SNTPComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SNTP..."); -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 if (sntp_enabled()) { sntp_stop(); } sntp_setoperatingmode(SNTP_OPMODE_POLL); #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 sntp_stop(); #endif @@ -51,9 +56,8 @@ void SNTPComponent::loop() { if (!time.is_valid()) return; - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; } diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 5475dc0a1f..b1362f5421 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -16,7 +16,7 @@ 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.domain), cv.Length(min=1, max=3) + cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py new file mode 100644 index 0000000000..8e9502be6d --- /dev/null +++ b/esphome/components/socket/__init__.py @@ -0,0 +1,28 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +CODEOWNERS = ["@esphome/core"] + +CONF_IMPLEMENTATION = "implementation" +IMPLEMENTATION_LWIP_TCP = "lwip_tcp" +IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" + +CONFIG_SCHEMA = cv.Schema( + { + cv.SplitDefault( + CONF_IMPLEMENTATION, + esp8266=IMPLEMENTATION_LWIP_TCP, + esp32=IMPLEMENTATION_BSD_SOCKETS, + ): cv.one_of( + IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" + ), + } +) + + +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_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 new file mode 100644 index 0000000000..1db24973e7 --- /dev/null +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -0,0 +1,161 @@ +#include "socket.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include + +#ifdef USE_ESP32 +#include +#include +#endif + +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 = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); + if (ret == nullptr) + return {}; + return std::string{buf}; + } else if (storage.ss_family == AF_INET6) { + const struct sockaddr_in6 *addr = reinterpret_cast(&storage); + char buf[INET6_ADDRSTRLEN]; + const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); + if (ret == nullptr) + return {}; + return std::string{buf}; + } + return {}; +} + +class BSDSocketImpl : public Socket { + public: + BSDSocketImpl(int fd) : Socket(), fd_(fd) {} + ~BSDSocketImpl() override { + if (!closed_) { + close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + } + } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = ::accept(fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd); + } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); } + int close() override { + int ret = ::close(fd_); + closed_ = true; + return ret; + } + int shutdown(int how) override { return ::shutdown(fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); } + std::string getpeername() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getpeername((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); } + std::string getsockname() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getsockname((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + return ::getsockopt(fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + return ::setsockopt(fd_, level, optname, optval, optlen); + } + 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 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 + return ::lwip_readv(fd_, iov, iovcnt); +#else + return ::readv(fd_, iov, iovcnt); +#endif + } + ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return ::send(fd_, buf, len, flags); } + ssize_t 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 + return ::lwip_writev(fd_, iov, iovcnt); +#else + return ::writev(fd_, iov, iovcnt); +#endif + } + int setblocking(bool blocking) override { + int fl = ::fcntl(fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + ::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 = ::socket(domain, type, protocol); + if (ret == -1) + return nullptr; + return std::unique_ptr{new BSDSocketImpl(ret)}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h new file mode 100644 index 0000000000..a383c0071d --- /dev/null +++ b/esphome/components/socket/headers.h @@ -0,0 +1,138 @@ +#pragma once +#include "esphome/core/defines.h" + +// Helper file to include all socket-related system headers (or use our own +// definitions where system ones don't exist) + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#define LWIP_INTERNAL +#include "lwip/inet.h" +#include +#include +#include + +/* Address families. */ +#define AF_UNSPEC 0 +#define AF_INET 2 +#define AF_INET6 10 +#define PF_INET AF_INET +#define PF_INET6 AF_INET6 +#define PF_UNSPEC AF_UNSPEC +#define IPPROTO_IP 0 +#define IPPROTO_TCP 6 +#define IPPROTO_IPV6 41 +#define IPPROTO_ICMPV6 58 + +#define TCP_NODELAY 0x01 + +#define F_GETFL 3 +#define F_SETFL 4 +#define O_NONBLOCK 1 + +#define SHUT_RD 0 +#define SHUT_WR 1 +#define SHUT_RDWR 2 + +/* Socket protocol types (TCP/UDP/RAW) */ +#define SOCK_STREAM 1 +#define SOCK_DGRAM 2 +#define SOCK_RAW 3 + +#define SO_REUSEADDR 0x0004 /* Allow local address reuse */ +#define SO_KEEPALIVE 0x0008 /* keep connections alive */ +#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ + +#define SOL_SOCKET 0xfff /* options for socket level */ + +using sa_family_t = uint8_t; +using in_port_t = uint16_t; + +// NOLINTNEXTLINE(readability-identifier-naming) +struct sockaddr_in { + uint8_t sin_len; + sa_family_t sin_family; + in_port_t sin_port; + struct in_addr sin_addr; +#define SIN_ZERO_LEN 8 + char sin_zero[SIN_ZERO_LEN]; +}; + +// NOLINTNEXTLINE(readability-identifier-naming) +struct sockaddr_in6 { + uint8_t sin6_len; /* length of this structure */ + sa_family_t sin6_family; /* AF_INET6 */ + in_port_t sin6_port; /* Transport layer port # */ + uint32_t sin6_flowinfo; /* IPv6 flow information */ + struct in6_addr sin6_addr; /* IPv6 address */ + uint32_t sin6_scope_id; /* Set of interfaces for scope */ +}; + +// NOLINTNEXTLINE(readability-identifier-naming) +struct sockaddr { + uint8_t sa_len; + sa_family_t sa_family; + char sa_data[14]; +}; + +// NOLINTNEXTLINE(readability-identifier-naming) +struct sockaddr_storage { + uint8_t s2_len; + sa_family_t ss_family; + char s2_data1[2]; + uint32_t s2_data2[3]; + uint32_t s2_data3[3]; +}; +using socklen_t = uint32_t; + +// NOLINTNEXTLINE(readability-identifier-naming) +struct iovec { + void *iov_base; + size_t iov_len; +}; + +#ifdef USE_ESP8266 +// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define +#ifdef INADDR_ANY +#undef INADDR_ANY +#endif +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif + +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !USE_ESP8266 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE +#endif + +#endif // USE_SOCKET_IMPL_LWIP_TCP + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include +#include +#include +#include +#include +#include +#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_BSD_SOCKETS diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp new file mode 100644 index 0000000000..54dfddac3f --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -0,0 +1,570 @@ +#include "socket.h" +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" +#include +#include +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace socket { + +static const char *const TAG = "socket.lwip"; + +// set to 1 to enable verbose lwip logging +#if 0 +#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) +#else +#define LWIP_LOG(msg, ...) +#endif + +class LWIPRawImpl : public Socket { + public: + LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {} + ~LWIPRawImpl() override { + if (pcb_ != nullptr) { + LWIP_LOG("tcp_abort(%p)", pcb_); + tcp_abort(pcb_); + pcb_ = nullptr; + } + } + + void init() { + LWIP_LOG("init(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); + tcp_err(pcb_, LWIPRawImpl::s_err_fn); + } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (accepted_sockets_.empty()) { + errno = EWOULDBLOCK; + return nullptr; + } + std::unique_ptr sock = std::move(accepted_sockets_.front()); + accepted_sockets_.pop(); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return std::unique_ptr(std::move(sock)); + } + int bind(const struct sockaddr *name, socklen_t addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { + errno = EINVAL; + return 0; + } + ip_addr_t ip; + in_port_t port; + auto family = name->sa_family; +#if LWIP_IPV6 + if (family == AF_INET) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.type = IPADDR_TYPE_V4; + ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; + + } else if (family == AF_INET6) { + if (addrlen < sizeof(sockaddr_in)) { + errno = EINVAL; + return -1; + } + auto *addr6 = reinterpret_cast(name); + port = ntohs(addr6->sin6_port); + ip.type = IPADDR_TYPE_V6; + memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + } else { + errno = EINVAL; + return -1; + } +#else + if (family != AF_INET) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.addr = addr4->sin_addr.s_addr; +#endif + LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); + err_t err = tcp_bind(pcb_, &ip, port); + if (err == ERR_USE) { + LWIP_LOG(" -> err ERR_USE"); + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + LWIP_LOG(" -> err ERR_VAL"); + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = EIO; + return -1; + } + return 0; + } + int close() override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + LWIP_LOG("tcp_close(%p)", pcb_); + err_t err = tcp_close(pcb_); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + tcp_abort(pcb_); + pcb_ = nullptr; + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + pcb_ = nullptr; + return 0; + } + int shutdown(int how) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + bool shut_rx = false, shut_tx = false; + if (how == SHUT_RD) { + shut_rx = true; + } else if (how == SHUT_WR) { + shut_tx = true; + } else if (how == SHUT_RDWR) { + shut_rx = shut_tx = true; + } else { + errno = EINVAL; + return -1; + } + LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); + err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return 0; + } + + int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->remote_port; + addr->sin_addr.s_addr = pcb_->remote_ip.addr; + return 0; + } + std::string getpeername() override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->remote_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); + return std::string(buffer); + } + int getsockname(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->local_port; + addr->sin_addr.s_addr = pcb_->local_ip.addr; + return 0; + } + std::string getsockname() override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->local_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); + return std::string(buffer); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (optlen == nullptr || optval == nullptr) { + errno = EINVAL; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + *reinterpret_cast(optval) = 1; + *optlen = 4; + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + *reinterpret_cast(optval) = nodelay_; + *optlen = 4; + return 0; + } + + errno = EINVAL; + return -1; + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + nodelay_ = val; + return 0; + } + + errno = EINVAL; + return -1; + } + int listen(int backlog) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = EOPNOTSUPP; + return -1; + } + // tcp_listen reallocates the pcb, replace ours + pcb_ = listen_pcb; + // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + return 0; + } + ssize_t read(void *buf, size_t len) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (rx_closed_ && rx_buf_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) { + return 0; + } + if (rx_buf_ == nullptr) { + errno = EWOULDBLOCK; + return -1; + } + + size_t read = 0; + uint8_t *buf8 = reinterpret_cast(buf); + while (len && rx_buf_ != nullptr) { + size_t pb_len = rx_buf_->len; + size_t pb_left = pb_len - rx_buf_offset_; + if (pb_left == 0) + break; + size_t copysize = std::min(len, pb_left); + memcpy(buf8, reinterpret_cast(rx_buf_->payload) + rx_buf_offset_, copysize); + + if (pb_left == copysize) { + // full pb copied, free it + if (rx_buf_->next == nullptr) { + // last buffer in chain + pbuf_free(rx_buf_); + rx_buf_ = nullptr; + rx_buf_offset_ = 0; + } else { + auto *old_buf = rx_buf_; + rx_buf_ = rx_buf_->next; + pbuf_ref(rx_buf_); + pbuf_free(old_buf); + rx_buf_offset_ = 0; + } + } else { + rx_buf_offset_ += copysize; + } + LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize); + tcp_recved(pcb_, copysize); + + buf8 += copysize; + len -= copysize; + read += copysize; + } + + return read; + } + ssize_t readv(const struct iovec *iov, int iovcnt) override { + ssize_t ret = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (ret != 0) + // if we already read some don't return an error + break; + return err; + } + ret += err; + if (err != iov[i].iov_len) + break; + } + return ret; + } + ssize_t internal_write(const void *buf, size_t len) { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) + return 0; + if (buf == nullptr) { + errno = EINVAL; + return 0; + } + auto space = tcp_sndbuf(pcb_); + if (space == 0) { + errno = EWOULDBLOCK; + return -1; + } + size_t to_send = std::min((size_t) space, len); + LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); + err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); + if (err == ERR_MEM) { + LWIP_LOG(" -> err ERR_MEM"); + errno = EWOULDBLOCK; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return to_send; + } + int internal_output() { + LWIP_LOG("tcp_output(%p)", pcb_); + err_t err = tcp_output(pcb_); + if (err == ERR_ABRT) { + LWIP_LOG(" -> err ERR_ABRT"); + // sometimes lwip returns ERR_ABRT for no apparent reason + // the connection works fine afterwards, and back with ESPAsyncTCP we + // indirectly also ignored this error + // FIXME: figure out where this is returned and what it means in this context + return 0; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return 0; + } + ssize_t write(const void *buf, size_t len) override { + ssize_t written = internal_write(buf, len); + if (written == -1) + return -1; + if (written == 0) + // no need to output if nothing written + return 0; + if (nodelay_) { + int err = internal_output(); + if (err == -1) + return -1; + } + return written; + } + ssize_t writev(const struct iovec *iov, int iovcnt) override { + ssize_t written = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (written != 0) + // if we already read some don't return an error + break; + return err; + } + written += err; + if (err != iov[i].iov_len) + break; + } + if (written == 0) + // no need to output if nothing written + return 0; + if (nodelay_) { + int err = internal_output(); + if (err == -1) + return -1; + } + return written; + } + int setblocking(bool blocking) override { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (blocking) { + // blocking operation not supported + errno = EINVAL; + return -1; + } + return 0; + } + + err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue + return ERR_OK; + } + auto sock = make_unique(newpcb); + sock->init(); + accepted_sockets_.push(std::move(sock)); + return ERR_OK; + } + void err_fn(err_t err) { + LWIP_LOG("err(err=%d)", err); + // "If a connection is aborted because of an error, the application is alerted of this event by + // the err callback." + // pcb is already freed when this callback is called + // ERR_RST: connection was reset by remote host + // ERR_ABRT: aborted through tcp_abort or TCP timer + pcb_ = nullptr; + } + err_t recv_fn(struct pbuf *pb, err_t err) { + LWIP_LOG("recv(pb=%p err=%d)", pb, err); + if (err != 0) { + // "An error code if there has been an error receiving Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + rx_closed_ = true; + return ERR_OK; + } + if (pb == nullptr) { + rx_closed_ = true; + return ERR_OK; + } + if (rx_buf_ == nullptr) { + // no need to copy because lwIP gave control of it to us + rx_buf_ = pb; + rx_buf_offset_ = 0; + } else { + pbuf_cat(rx_buf_, pb); + } + return ERR_OK; + } + + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn(newpcb, err); + } + + static void s_err_fn(void *arg, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + arg_this->err_fn(err); + } + + static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->recv_fn(pb, err); + } + + protected: + struct tcp_pcb *pcb_; + std::queue> accepted_sockets_; + bool rx_closed_ = false; + pbuf *rx_buf_ = nullptr; + size_t rx_buf_offset_ = 0; + // don't use lwip nodelay flag, it sometimes causes reconnect + // instead use it for determining whether to call lwip_output + bool nodelay_ = false; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + auto *pcb = tcp_new(); + if (pcb == nullptr) + return nullptr; + auto *sock = new LWIPRawImpl(pcb); // NOLINT(cppcoreguidelines-owning-memory) + sock->init(); + return std::unique_ptr{sock}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h new file mode 100644 index 0000000000..9920610bf5 --- /dev/null +++ b/esphome/components/socket/socket.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +#include "headers.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace socket { + +class Socket { + public: + Socket() = default; + virtual ~Socket() = default; + Socket(const Socket &) = delete; + Socket &operator=(const Socket &) = delete; + + virtual std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int close() = 0; + // not supported yet: + // virtual int connect(const std::string &address) = 0; + // virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int shutdown(int how) = 0; + + virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getpeername() = 0; + virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getsockname() = 0; + virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; + virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; + virtual int listen(int backlog) = 0; + virtual ssize_t read(void *buf, size_t len) = 0; + 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; + virtual int setblocking(bool blocking) = 0; + virtual int loop() { return 0; }; +}; + +std::unique_ptr socket(int domain, int type, int protocol); + +} // namespace socket +} // namespace esphome diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 8c6ec54d4c..cb10db4ed4 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -56,7 +56,10 @@ void SpeedFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float SpeedFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace speed } // namespace esphome diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index e6e073c4a4..3a96cce99b 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import pins from esphome.const import ( CONF_CLK_PIN, @@ -9,7 +10,7 @@ from esphome.const import ( CONF_SPI_ID, CONF_CS_PIN, ) -from esphome.core import coroutine_with_priority +from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@esphome/core"] spi_ns = cg.esphome_ns.namespace("spi") @@ -45,6 +46,9 @@ async def to_code(config): mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) + if CORE.is_esp32: + cg.add_library("SPI", None) + def spi_device_schema(cs_pin_required=True): """Create a schema for an SPI device. @@ -69,9 +73,24 @@ async def register_spi_device(var, config): cg.add(var.set_cs_pin(pin)) -def validate_device(name, config, item_config, require_mosi, require_miso): - spi_config = config.get_config_by_id(item_config[CONF_SPI_ID]) - if require_mosi and CONF_MISO_PIN not in spi_config: - raise ValueError(f"Component {name} requires parent spi to declare miso_pin") - if require_miso and CONF_MOSI_PIN not in spi_config: - raise ValueError(f"Component {name} requires parent spi to declare mosi_pin") +def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): + hub_schema = {} + if require_miso: + hub_schema[ + cv.Required( + CONF_MISO_PIN, + msg=f"Component {name} requires this spi bus to declare a miso_pin", + ) + ] = cv.valid + if require_mosi: + hub_schema[ + cv.Required( + CONF_MOSI_PIN, + msg=f"Component {name} requires this spi bus to declare a mosi_pin", + ) + ] = cv.valid + + return cv.Schema( + {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)}, + extra=cv.ALLOW_EXTRA, + ) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index a5d7fba30a..d883142c81 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -8,12 +8,13 @@ namespace spi { static const char *const TAG = "spi"; -void ICACHE_RAM_ATTR HOT SPIComponent::disable() { +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_) { - ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); this->active_cs_->digital_write(true); this->active_cs_ = nullptr; } @@ -23,19 +24,37 @@ void SPIComponent::setup() { this->clk_->setup(); this->clk_->digital_write(true); +#ifdef USE_SPI_ARDUINO_BACKEND bool use_hw_spi = true; - if (this->clk_->is_inverted()) - use_hw_spi = false; const bool has_miso = this->miso_ != nullptr; const bool has_mosi = this->mosi_ != nullptr; - if (has_miso && this->miso_->is_inverted()) + int8_t clk_pin = -1, miso_pin = -1, mosi_pin = -1; + + if (!this->clk_->is_internal()) use_hw_spi = false; - if (has_mosi && this->mosi_->is_inverted()) + if (has_miso && !miso_->is_internal()) use_hw_spi = false; - int8_t clk_pin = this->clk_->get_pin(); - int8_t miso_pin = has_miso ? this->miso_->get_pin() : -1; - int8_t mosi_pin = has_mosi ? this->mosi_->get_pin() : -1; -#ifdef ARDUINO_ARCH_ESP8266 + 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) { // pass } else if (clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13)) { @@ -50,8 +69,8 @@ void SPIComponent::setup() { this->hw_spi_->begin(); return; } -#endif -#ifdef ARDUINO_ARCH_ESP32 +#endif // USE_ESP8266 +#ifdef USE_ESP32 static uint8_t spi_bus_num = 0; if (spi_bus_num >= 2) { use_hw_spi = false; @@ -61,13 +80,14 @@ void SPIComponent::setup() { if (spi_bus_num == 0) { this->hw_spi_ = &SPI; } else { - this->hw_spi_ = new SPIClass(VSPI); + this->hw_spi_ = new SPIClass(VSPI); // NOLINT(cppcoreguidelines-owning-memory) } spi_bus_num++; this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); return; } -#endif +#endif // USE_ESP32 +#endif // USE_SPI_ARDUINO_BACKEND if (this->miso_ != nullptr) { this->miso_->setup(); @@ -82,32 +102,28 @@ void SPIComponent::dump_config() { 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::debug_tx(uint8_t value) { - ESP_LOGVV(TAG, " TX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); -} -void SPIComponent::debug_rx(uint8_t value) { - ESP_LOGVV(TAG, " RX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); -} -void SPIComponent::debug_enable(uint8_t pin) { ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", pin); } - void SPIComponent::cycle_clock_(bool value) { - uint32_t start = ESP.getCycleCount(); - while (start - ESP.getCycleCount() < this->wait_cycle_) + 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 - ESP.getCycleCount() < this->wait_cycle_) + while (start - arch_get_cpu_cycle_count() < this->wait_cycle_) ; } // NOLINTNEXTLINE +#ifndef CLANG_TIDY #pragma GCC optimize("unroll-loops") // NOLINTNEXTLINE #pragma GCC optimize("O2") +#endif // CLANG_TIDY template uint8_t HOT SPIComponent::transfer_(uint8_t data) { @@ -153,15 +169,6 @@ uint8_t HOT SPIComponent::transfer_(uint8_t data) { } } -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - if (WRITE) { - SPIComponent::debug_tx(data); - } - if (READ) { - SPIComponent::debug_rx(out_data); - } -#endif - App.feed_wdt(); return out_data; diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index a4a2e11def..601a5c5a7e 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,8 +1,16 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" +#include + +#ifdef USE_ARDUINO +#define USE_SPI_ARDUINO_BACKEND +#endif + +#ifdef USE_SPI_ARDUINO_BACKEND #include +#endif namespace esphome { namespace spi { @@ -72,18 +80,22 @@ class SPIComponent : public Component { 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(); } @@ -91,19 +103,23 @@ class SPIComponent : public Component { template void write_byte(uint8_t data) { +#ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { this->hw_spi_->write(data); 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) { this->hw_spi_->write16(data); return; } +#endif // USE_SPI_ARDUINO_BACKEND this->write_byte(data >> 8); this->write_byte(data); @@ -111,12 +127,14 @@ class SPIComponent : public Component { 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++) { this->hw_spi_->write16(data[i]); } return; } +#endif // USE_SPI_ARDUINO_BACKEND for (size_t i = 0; i < length; i++) { this->write_byte16(data[i]); } @@ -124,11 +142,13 @@ class SPIComponent : public Component { 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); this->hw_spi_->writeBytes(data_c, length); return; } +#endif // USE_SPI_ARDUINO_BACKEND for (size_t i = 0; i < length; i++) { this->write_byte(data[i]); } @@ -136,6 +156,7 @@ class SPIComponent : public Component { template uint8_t transfer_byte(uint8_t data) { +#ifdef USE_SPI_ARDUINO_BACKEND if (this->miso_ != nullptr) { if (this->hw_spi_ != nullptr) { return this->hw_spi_->transfer(data); @@ -143,12 +164,14 @@ class SPIComponent : public Component { return this->transfer_(data); } } +#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); @@ -157,6 +180,7 @@ class SPIComponent : public Component { } return; } +#endif // USE_SPI_ARDUINO_BACKEND if (this->miso_ != nullptr) { for (size_t i = 0; i < length; i++) { @@ -169,18 +193,19 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - if (cs != nullptr) { - SPIComponent::debug_enable(cs->get_pin()); - } - +#ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); this->hw_spi_->beginTransaction(settings); } else { +#endif // USE_SPI_ARDUINO_BACKEND this->clk_->digital_write(CLOCK_POLARITY); - this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; + 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; @@ -195,10 +220,6 @@ class SPIComponent : public Component { protected: inline void cycle_clock_(bool value); - static void debug_enable(uint8_t pin); - static void debug_tx(uint8_t value); - static void debug_rx(uint8_t value); - template uint8_t transfer_(uint8_t data); @@ -206,7 +227,9 @@ class SPIComponent : public Component { GPIOPin *miso_{nullptr}; GPIOPin *mosi_{nullptr}; GPIOPin *active_cs_{nullptr}; +#ifdef USE_SPI_ARDUINO_BACKEND SPIClass *hw_spi_{nullptr}; +#endif // USE_SPI_ARDUINO_BACKEND uint32_t wait_cycle_; }; @@ -246,7 +269,7 @@ class SPIDevice { return this->parent_->template write_byte(data); } - void write_byte16(uint8_t data) { + void write_byte16(uint16_t data) { return this->parent_->template write_byte16(data); } diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 219f68c5c8..27264cf942 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -13,7 +13,9 @@ from esphome.const import ( CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, @@ -33,74 +35,67 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SPS30Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_4_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_4_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_SIZE): sensor.sensor_schema( - UNIT_MICROMETER, - ICON_RULER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 30bd134626..472b7606ed 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -242,10 +242,9 @@ bool SPS30Component::start_continuous_measurement_() { bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -254,13 +253,11 @@ bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sht_crc_(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 9652d01efa..bc2e558f1b 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -8,12 +8,19 @@ from esphome.const import ( CONF_MODEL, CONF_RESET_PIN, CONF_BRIGHTNESS, + CONF_CONTRAST, + CONF_INVERT, ) ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer) SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") +CONF_FLIP_X = "flip_x" +CONF_FLIP_Y = "flip_y" +CONF_OFFSET_X = "offset_x" +CONF_OFFSET_Y = "offset_y" + MODELS = { "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, @@ -23,21 +30,44 @@ MODELS = { "SH1106_128X64": SSD1306Model.SH1106_MODEL_128_64, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, + "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, + "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") + +def _validate(value): + model = value[CONF_MODEL] + if model not in ("SSD1305_128X32", "SSD1305_128X64"): + # Contrast is default value (1.0) while brightness is not + # Indicates user is using old `brightness` option + if value[CONF_BRIGHTNESS] != 1.0 and value[CONF_CONTRAST] == 1.0: + raise cv.Invalid( + "SSD1306/SH1106 no longer accepts brightness option, " + 'please use "contrast" instead.' + ) + + return value + + SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( { cv.Required(CONF_MODEL): SSD1306_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_CONTRAST, default=1.0): cv.percentage, 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=0, max=15), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) -async def setup_ssd1036(var, config): +async def setup_ssd1306(var, config): await cg.register_component(var, config) await display.register_display(var, config) @@ -47,8 +77,20 @@ async def setup_ssd1036(var, config): cg.add(var.set_reset_pin(reset)) if CONF_BRIGHTNESS in config: cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_CONTRAST in config: + cg.add(var.init_contrast(config[CONF_CONTRAST])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_FLIP_X in config: + cg.add(var.init_flip_x(config[CONF_FLIP_X])) + if CONF_FLIP_Y in config: + cg.add(var.init_flip_y(config[CONF_FLIP_X])) + if CONF_OFFSET_X in config: + cg.add(var.init_offset_x(config[CONF_OFFSET_X])) + if CONF_OFFSET_Y in config: + cg.add(var.init_offset_y(config[CONF_OFFSET_Y])) + if CONF_INVERT in 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 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 58f86fd182..b1a2538ebd 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -7,15 +7,14 @@ namespace ssd1306_base { static const char *const TAG = "ssd1306"; -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; static const uint8_t SSD1306_MAX_CONTRAST = 255; +static const uint8_t SSD1305_MAX_BRIGHTNESS = 255; static const uint8_t SSD1306_COMMAND_DISPLAY_OFF = 0xAE; static const uint8_t SSD1306_COMMAND_DISPLAY_ON = 0xAF; static const uint8_t SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV = 0xD5; static const uint8_t SSD1306_COMMAND_SET_MULTIPLEX = 0xA8; -static const uint8_t SSD1306_COMMAND_SET_DISPLAY_OFFSET = 0xD3; +static const uint8_t SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y = 0xD3; static const uint8_t SSD1306_COMMAND_SET_START_LINE = 0x40; static const uint8_t SSD1306_COMMAND_CHARGE_PUMP = 0x8D; static const uint8_t SSD1306_COMMAND_MEMORY_MODE = 0x20; @@ -30,33 +29,60 @@ static const uint8_t SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME = 0xA4; static const uint8_t SSD1306_COMMAND_DEACTIVATE_SCROLL = 0x2E; static const uint8_t SSD1306_COMMAND_COLUMN_ADDRESS = 0x21; static const uint8_t SSD1306_COMMAND_PAGE_ADDRESS = 0x22; +static const uint8_t SSD1306_COMMAND_NORMAL_DISPLAY = 0xA6; +static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7; -static const uint8_t SSD1306_NORMAL_DISPLAY = 0xA6; +static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; +static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); + // Turn off display during initialization (0xAE) this->command(SSD1306_COMMAND_DISPLAY_OFF); - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - this->command(0x80); // suggested ratio + // 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_()) { + this->command(SSD1305_COMMAND_SET_AREA_COLOR); + this->command(0x05); + } + + // Set mux ratio to [Y pixels - 1] (0xA8) this->command(SSD1306_COMMAND_SET_MULTIPLEX); this->command(this->get_height_internal() - 1); - this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET); - this->command(0x00); // no offset - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); // start at line 0 - this->command(SSD1306_COMMAND_CHARGE_PUMP); - if (this->external_vcc_) - this->command(0x10); - else - this->command(0x14); + // Set Y offset (0xD3) + 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); + // SSD1305 does not have charge pump + if (!this->is_ssd1305_()) { + // Enable charge pump (0x8D) + this->command(SSD1306_COMMAND_CHARGE_PUMP); + if (this->external_vcc_) + this->command(0x10); + else + this->command(0x14); + } + + // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); this->command(0x00); - this->command(SSD1306_COMMAND_SEGRE_MAP | 0x01); - this->command(SSD1306_COMMAND_COM_SCAN_DEC); + // 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: @@ -69,28 +95,40 @@ void SSD1306::setup() { case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: this->command(0x12); break; } + // Pre-charge period (0xD9) this->command(SSD1306_COMMAND_SET_PRE_CHARGE); if (this->external_vcc_) this->command(0x22); else this->command(0xF1); + // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); this->command(0x00); + // Display output follow RAM (0xA4) this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); - this->command(SSD1306_NORMAL_DISPLAY); + // Inverse display mode (0xA6, 0xA7) + this->command(SSD1306_COMMAND_NORMAL_DISPLAY | this->invert_); + + // Disable scrolling mode (0x2E) this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); - set_brightness(this->brightness_); + // Contrast and brighrness + // SSD1306 does not have brightness setting + set_contrast(this->contrast_); + if (this->is_ssd1305_()) + set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory this->turn_on(); } @@ -103,12 +141,12 @@ void SSD1306::display() { this->command(SSD1306_COMMAND_COLUMN_ADDRESS); switch (this->model_) { case SSD1306_MODEL_64_48: - this->command(0x20); - this->command(0x20 + this->get_width_internal() - 1); + this->command(0x20 + this->offset_x_); + this->command(0x20 + this->offset_x_ + this->get_width_internal() - 1); break; default: - this->command(0); // Page start address, 0 - this->command(this->get_width_internal() - 1); + this->command(0 + this->offset_x_); // Page start address, 0 + this->command(this->get_width_internal() + this->offset_x_ - 1); break; } @@ -124,16 +162,28 @@ 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_ssd1305_() const { + return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; +} void SSD1306::update() { this->do_update_(); this->display(); } +void SSD1306::set_contrast(float contrast) { + // validation + this->contrast_ = clamp(contrast, 0.0F, 1.0F); + // now write the new contrast level to the display (0x81) + this->command(SSD1306_COMMAND_SET_CONTRAST); + this->command(int(SSD1306_MAX_CONTRAST * (this->contrast_))); +} void SSD1306::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); - // now write the new brightness level to the display - this->command(SSD1306_COMMAND_SET_CONTRAST); - this->command(int(SSD1306_MAX_CONTRAST * (this->brightness_))); + if (!this->is_ssd1305_()) + return; + this->brightness_ = clamp(brightness, 0.0F, 1.0F); + // now write the new brightness level to the display (0x82) + this->command(SSD1305_COMMAND_SET_BRIGHTNESS); + this->command(int(SSD1305_MAX_BRIGHTNESS * (this->brightness_))); } bool SSD1306::is_on() { return this->is_on_; } void SSD1306::turn_on() { @@ -148,9 +198,11 @@ int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: case SH1106_MODEL_128_32: + case SSD1305_MODEL_128_32: return 32; case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: + case SSD1305_MODEL_128_64: return 64; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -168,6 +220,8 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_32: case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -229,6 +283,10 @@ const char *SSD1306::model_str_() { return "SH1106 96x16"; case SH1106_MODEL_64_48: return "SH1106 64x48"; + case SSD1305_MODEL_128_32: + return "SSD1305 128x32"; + case SSD1305_MODEL_128_64: + return "SSD1305 128x32"; default: return "Unknown"; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 0fe09709e7..09417a2c10 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { @@ -16,6 +16,8 @@ enum SSD1306Model { SH1106_MODEL_128_64, SH1106_MODEL_96_16, SH1106_MODEL_64_48, + SSD1305_MODEL_128_32, + SSD1305_MODEL_128_64, }; class SSD1306 : public PollingComponent, public display::DisplayBuffer { @@ -29,8 +31,15 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model model) { this->model_ = model; } 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; } + void set_contrast(float contrast); 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; } + void init_flip_y(bool flip_y) { this->flip_y_ = flip_y; } + 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; } bool is_on(); void turn_on(); void turn_off(); @@ -43,6 +52,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; @@ -55,7 +65,13 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; bool is_on_{false}; + float contrast_{1.0}; float brightness_{1.0}; + bool flip_x_{true}; + bool flip_y_{true}; + uint8_t offset_x_{0}; + uint8_t offset_y_{0}; + bool invert_{false}; }; } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_i2c/display.py b/esphome/components/ssd1306_i2c/display.py index 4b51a90431..c51ab5f93e 100644 --- a/esphome/components/ssd1306_i2c/display.py +++ b/esphome/components/ssd1306_i2c/display.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import ssd1306_base, i2c +from esphome.components.ssd1306_base import _validate from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -18,10 +19,11 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x3C)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await ssd1306_base.setup_ssd1036(var, config) + await ssd1306_base.setup_ssd1306(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index fce9796008..fddea25fc8 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -10,8 +10,8 @@ void I2CSSD1306::setup() { ESP_LOGCONFIG(TAG, "Setting up I2C SSD1306..."); this->init_reset_(); - this->raw_begin_transmission(); - if (!this->raw_end_transmission()) { + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; @@ -25,6 +25,11 @@ void I2CSSD1306::dump_config() { ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + ESP_LOGCONFIG(TAG, " Flip X: %s", YESNO(this->flip_x_)); + ESP_LOGCONFIG(TAG, " Flip Y: %s", YESNO(this->flip_y_)); + ESP_LOGCONFIG(TAG, " Offset X: %d", this->offset_x_); + ESP_LOGCONFIG(TAG, " Offset Y: %d", this->offset_y_); + ESP_LOGCONFIG(TAG, " Inverted Color: %s", YESNO(this->invert_)); LOG_UPDATE_INTERVAL(this); if (this->error_code_ == COMMUNICATION_FAILED) { diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index f7dd1553ba..0af1168bde 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import spi, ssd1306_base +from esphome.components.ssd1306_base import _validate from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -20,12 +21,13 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await ssd1306_base.setup_ssd1036(var, config) + await ssd1306_base.setup_ssd1306(var, config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 5ef25b8139..33d474a8ee 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -22,6 +22,11 @@ void SPISSD1306::dump_config() { LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + ESP_LOGCONFIG(TAG, " Flip X: %s", YESNO(this->flip_x_)); + ESP_LOGCONFIG(TAG, " Flip Y: %s", YESNO(this->flip_y_)); + ESP_LOGCONFIG(TAG, " Offset X: %d", this->offset_x_); + ESP_LOGCONFIG(TAG, " Offset Y: %d", this->offset_y_); + ESP_LOGCONFIG(TAG, " Inverted Color: %s", YESNO(this->invert_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1306::command(uint8_t value) { diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 0a3233acfe..520248a66e 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -106,9 +106,9 @@ void SSD1322::setup() { this->data(180); this->command(SSD1322_ENABLEGRAYSCALETABLE); set_brightness(this->brightness_); - this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1322::display() { this->command(SSD1322_SETCOLUMNADDRESS); // set column address @@ -126,7 +126,7 @@ void SSD1322::update() { this->display(); } void SSD1322::set_brightness(float brightness) { - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1322_SETCONTRAST); this->data(int(SSD1322_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h index 125e374246..6a790c0199 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.h +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 1cca1853b1..60e46f573f 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -7,8 +7,6 @@ namespace ssd1325_base { static const char *const TAG = "ssd1325"; -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 15; static const uint8_t SSD1325_MAX_CONTRAST = 127; static const uint8_t SSD1325_COLORMASK = 0x0f; static const uint8_t SSD1325_COLORSHIFT = 4; @@ -114,9 +112,9 @@ void SSD1325::setup() { this->command(0x0D | 0x02); this->command(SSD1325_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1325::display() { this->command(SSD1325_SETCOLADDR); // set column address diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index a06ba69a59..cca9412c43 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 798f67e4fc..4cb8d17a3d 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -78,9 +78,9 @@ void SSD1327::setup() { this->command(0x1C); this->command(SSD1327_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); - this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1327::display() { this->command(SSD1327_SETCOLUMNADDRESS); // set column address @@ -100,7 +100,7 @@ void SSD1327::update() { } void SSD1327::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1327_SETCONTRAST); this->command(int(SSD1327_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h index 03f360b258..35b021c71b 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.h +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp index 2967d2f9c4..e9e047bfb6 100644 --- a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp @@ -10,8 +10,8 @@ void I2CSSD1327::setup() { ESP_LOGCONFIG(TAG, "Setting up I2C SSD1327..."); this->init_reset_(); - this->raw_begin_transmission(); - if (!this->raw_end_transmission()) { + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index a4bbad508c..88764c3d90 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -7,8 +7,6 @@ namespace ssd1331_base { static const char *const TAG = "ssd1331"; -static const uint16_t BLACK = 0; -static const uint16_t WHITE = 0xffff; static const uint16_t SSD1331_COLORMASK = 0xffff; static const uint8_t SSD1331_MAX_CONTRASTA = 0x91; static const uint8_t SSD1331_MAX_CONTRASTB = 0x50; @@ -19,7 +17,7 @@ static const uint8_t SSD1331_DRAWLINE = 0x21; // Draw line static const uint8_t SSD1331_DRAWRECT = 0x22; // Draw rectangle static const uint8_t SSD1331_FILL = 0x26; // Fill enable/disable static const uint8_t SSD1331_SETCOLUMN = 0x15; // Set column address -static const uint8_t SSD1331_SETROW = 0x75; // Set row adress +static const uint8_t SSD1331_SETROW = 0x75; // Set row address static const uint8_t SSD1331_CONTRASTA = 0x81; // Set contrast for color A static const uint8_t SSD1331_CONTRASTB = 0x82; // Set contrast for color B static const uint8_t SSD1331_CONTRASTC = 0x83; // Set contrast for color C @@ -78,9 +76,9 @@ void SSD1331::setup() { this->command(SSD1331_MASTERCURRENT); // 0x87 this->command(0x06); set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1331::display() { this->command(SSD1331_SETCOLUMN); // set column address @@ -97,7 +95,7 @@ void SSD1331::update() { } void SSD1331::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1331_CONTRASTA); // 0x81 this->command(int(SSD1331_MAX_CONTRASTA * (this->brightness_))); diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h index 8d2bca5de0..b889a47fbe 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.h +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 34f357e38a..f26cd7c697 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -7,8 +7,6 @@ namespace ssd1351_base { static const char *const TAG = "ssd1351"; -static const uint16_t BLACK = 0; -static const uint16_t WHITE = 0xffff; static const uint16_t SSD1351_COLORMASK = 0xffff; static const uint8_t SSD1351_MAX_CONTRAST = 15; static const uint8_t SSD1351_BYTESPERPIXEL = 2; @@ -87,9 +85,9 @@ void SSD1351::setup() { this->data(0x80); this->data(0xC8); set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1351::display() { this->command(SSD1351_SETCOLUMN); // set column address diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h index 2730f798b5..422e601f8b 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.h +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" namespace esphome { diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index c1ede4e0ce..ae31f604a5 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -23,6 +23,7 @@ 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 @@ -58,6 +59,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_ROW_START): cv.int_, cv.Optional(CONF_EIGHT_BIT_COLOR, default=False): cv.boolean, cv.Optional(CONF_USE_BGR, default=False): cv.boolean, + cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, } ) .extend(cv.COMPONENT_SCHEMA) @@ -90,6 +92,7 @@ async def to_code(config): config[CONF_ROW_START], config[CONF_EIGHT_BIT_COLOR], config[CONF_USE_BGR], + config[CONF_INVERT_COLORS], ) await setup_st7735(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index f329ef4620..8490aa1fe4 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -1,6 +1,7 @@ #include "st7735.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace st7735 { @@ -220,16 +221,16 @@ static const uint8_t PROGMEM // clang-format on static const char *const TAG = "st7735"; -ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, - boolean usebgr) { - model_ = model; - this->width_ = width; - this->height_ = height; - this->colstart_ = colstart; - this->rowstart_ = rowstart; - this->eightbitcolor_ = eightbitcolor; - this->usebgr_ = usebgr; -} +ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr, + bool invert_colors) + : model_(model), + colstart_(colstart), + rowstart_(rowstart), + eightbitcolor_(eightbitcolor), + usebgr_(usebgr), + invert_colors_(invert_colors), + width_(width), + height_(height) {} void ST7735::setup() { ESP_LOGCONFIG(TAG, "Setting up ST7735..."); @@ -283,6 +284,9 @@ void ST7735::setup() { } sendcommand_(ST77XX_MADCTL, &data, 1); + if (this->invert_colors_) + sendcommand_(ST77XX_INVON, nullptr, 0); + this->init_internal_(this->get_buffer_length()); memset(this->buffer_, 0x00, this->get_buffer_length()); } @@ -350,17 +354,17 @@ void ST7735::display_init_(const uint8_t *addr) { uint8_t num_commands, cmd, num_args; uint16_t ms; - num_commands = pgm_read_byte(addr++); // Number of commands to follow - while (num_commands--) { // For each command... - cmd = pgm_read_byte(addr++); // Read command - num_args = pgm_read_byte(addr++); // Number of args to follow - ms = num_args & ST_CMD_DELAY; // If hibit set, delay follows args - num_args &= ~ST_CMD_DELAY; // Mask out delay bit + num_commands = progmem_read_byte(addr++); // Number of commands to follow + while (num_commands--) { // For each command... + cmd = progmem_read_byte(addr++); // Read command + num_args = progmem_read_byte(addr++); // Number of args to follow + ms = num_args & ST_CMD_DELAY; // If hibit set, delay follows args + num_args &= ~ST_CMD_DELAY; // Mask out delay bit this->sendcommand_(cmd, addr, num_args); addr += num_args; if (ms) { - ms = pgm_read_byte(addr++); // Read post-command delay time (ms) + ms = progmem_read_byte(addr++); // Read post-command delay time (ms) if (ms == 255) ms = 500; // If 255, delay for 500 ms delay(ms); @@ -407,7 +411,7 @@ void HOT ST7735::senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes) { this->cs_->digital_write(false); this->enable(); for (uint8_t i = 0; i < num_data_bytes; i++) { - this->write_byte(pgm_read_byte(data_bytes++)); // write byte - SPI library + this->write_byte(progmem_read_byte(data_bytes++)); // write byte - SPI library } this->cs_->digital_write(true); this->disable(); @@ -460,26 +464,26 @@ void HOT ST7735::write_display_data_() { } void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t BYTE[4]; - BYTE[0] = (addr1 >> 8) & 0xFF; - BYTE[1] = addr1 & 0xFF; - BYTE[2] = (addr2 >> 8) & 0xFF; - BYTE[3] = addr2 & 0xFF; + static uint8_t byte[4]; + byte[0] = (addr1 >> 8) & 0xFF; + byte[1] = addr1 & 0xFF; + byte[2] = (addr2 >> 8) & 0xFF; + byte[3] = addr2 & 0xFF; this->dc_pin_->digital_write(true); - this->write_array(BYTE, 4); + this->write_array(byte, 4); } void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) { - static uint8_t BYTE[1024]; + static uint8_t byte[1024]; int index = 0; for (int i = 0; i < size; i++) { - BYTE[index++] = (color >> 8) & 0xFF; - BYTE[index++] = color & 0xFF; + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; } this->dc_pin_->digital_write(true); - return write_array(BYTE, size * 2); + return write_array(byte, size * 2); } } // namespace st7735 diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 11bcc746f0..c049fb9e83 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -1,87 +1,89 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/display/display_buffer.h" - -namespace esphome { -namespace st7735 { - -static const uint8_t ST7735_TFTWIDTH_128 = 128; // for 1.44 and mini^M -static const uint8_t ST7735_TFTWIDTH_80 = 80; // for mini^M -static const uint8_t ST7735_TFTHEIGHT_128 = 128; // for 1.44" display^M -static const uint8_t ST7735_TFTHEIGHT_160 = 160; // for 1.8" and mini display^M - -// some flags for initR() :( -static const uint8_t INITR_GREENTAB = 0x00; -static const uint8_t INITR_REDTAB = 0x01; -static const uint8_t INITR_BLACKTAB = 0x02; -static const uint8_t INITR_144GREENTAB = 0x01; -static const uint8_t INITR_MINI_160X80 = 0x04; -static const uint8_t INITR_HALLOWING = 0x05; -static const uint8_t INITR_18GREENTAB = INITR_GREENTAB; -static const uint8_t INITR_18REDTAB = INITR_REDTAB; -static const uint8_t INITR_18BLACKTAB = INITR_BLACKTAB; - -enum ST7735Model { - ST7735_INITR_GREENTAB = INITR_GREENTAB, - ST7735_INITR_REDTAB = INITR_REDTAB, - ST7735_INITR_BLACKTAB = INITR_BLACKTAB, - ST7735_INITR_MINI_160X80 = INITR_MINI_160X80, - ST7735_INITR_18BLACKTAB = INITR_18BLACKTAB, - ST7735_INITR_18REDTAB = INITR_18REDTAB -}; - -class ST7735 : public PollingComponent, - public display::DisplayBuffer, - public spi::SPIDevice { - public: - ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, boolean usebgr); - void dump_config() override; - void setup() override; - - void display(); - - void update() override; - - void set_model(ST7735Model model) { this->model_ = model; } - float get_setup_priority() const override { return setup_priority::PROCESSOR; } - - void set_reset_pin(GPIOPin *value) { this->reset_pin_ = value; } - void set_dc_pin(GPIOPin *value) { dc_pin_ = value; } - size_t get_buffer_length(); - - protected: - void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes); - void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes); - - void writecommand_(uint8_t value); - void writedata_(uint8_t value); - - void write_display_data_(); - - void init_reset_(); - void display_init_(const uint8_t *addr); - void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); - void draw_absolute_pixel_internal(int x, int y, Color color) override; - void spi_master_write_addr_(uint16_t addr1, uint16_t addr2); - void spi_master_write_color_(uint16_t color, uint16_t size); - - int get_width_internal() override; - int get_height_internal() override; - - const char *model_str_(); - - ST7735Model model_{ST7735_INITR_18BLACKTAB}; - uint8_t colstart_ = 0, rowstart_ = 0; - boolean eightbitcolor_ = false; - boolean usebgr_ = false; - int16_t width_ = 80, height_ = 80; // Watch heap size - - GPIOPin *reset_pin_{nullptr}; - GPIOPin *dc_pin_{nullptr}; -}; - -} // namespace st7735 -} // namespace esphome +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7735 { + +static const uint8_t ST7735_TFTWIDTH_128 = 128; // for 1.44 and mini^M +static const uint8_t ST7735_TFTWIDTH_80 = 80; // for mini^M +static const uint8_t ST7735_TFTHEIGHT_128 = 128; // for 1.44" display^M +static const uint8_t ST7735_TFTHEIGHT_160 = 160; // for 1.8" and mini display^M + +// some flags for initR() :( +static const uint8_t INITR_GREENTAB = 0x00; +static const uint8_t INITR_REDTAB = 0x01; +static const uint8_t INITR_BLACKTAB = 0x02; +static const uint8_t INITR_144GREENTAB = 0x01; +static const uint8_t INITR_MINI_160X80 = 0x04; +static const uint8_t INITR_HALLOWING = 0x05; +static const uint8_t INITR_18GREENTAB = INITR_GREENTAB; +static const uint8_t INITR_18REDTAB = INITR_REDTAB; +static const uint8_t INITR_18BLACKTAB = INITR_BLACKTAB; + +enum ST7735Model { + ST7735_INITR_GREENTAB = INITR_GREENTAB, + ST7735_INITR_REDTAB = INITR_REDTAB, + ST7735_INITR_BLACKTAB = INITR_BLACKTAB, + ST7735_INITR_MINI_160X80 = INITR_MINI_160X80, + ST7735_INITR_18BLACKTAB = INITR_18BLACKTAB, + ST7735_INITR_18REDTAB = INITR_18REDTAB +}; + +class ST7735 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr, + bool invert_colors); + void dump_config() override; + void setup() override; + + void display(); + + void update() override; + + void set_model(ST7735Model model) { this->model_ = model; } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + void set_reset_pin(GPIOPin *value) { this->reset_pin_ = value; } + void set_dc_pin(GPIOPin *value) { dc_pin_ = value; } + size_t get_buffer_length(); + + protected: + void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes); + void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes); + + void writecommand_(uint8_t value); + void writedata_(uint8_t value); + + void write_display_data_(); + + void init_reset_(); + void display_init_(const uint8_t *addr); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void spi_master_write_addr_(uint16_t addr1, uint16_t addr2); + void spi_master_write_color_(uint16_t color, uint16_t size); + + int get_width_internal() override; + int get_height_internal() override; + + const char *model_str_(); + + ST7735Model model_{ST7735_INITR_18BLACKTAB}; + uint8_t colstart_ = 0, rowstart_ = 0; + bool eightbitcolor_ = false; + bool usebgr_ = false; + bool invert_colors_ = false; + int16_t width_ = 80, height_ = 80; // Watch heap size + + GPIOPin *reset_pin_{nullptr}; + GPIOPin *dc_pin_{nullptr}; +}; + +} // namespace st7735 +} // namespace esphome diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a053d00ea2..7b38b1d2c5 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = ( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, } ) @@ -49,8 +49,9 @@ async def to_code(config): reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) - bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) - cg.add(var.set_backlight_pin(bl)) + if CONF_BACKLIGHT_PIN in config: + 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( diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index c84153970d..471ad6664c 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -197,26 +197,26 @@ void ST7789V::write_data_(uint8_t value) { } void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t BYTE[4]; - BYTE[0] = (addr1 >> 8) & 0xFF; - BYTE[1] = addr1 & 0xFF; - BYTE[2] = (addr2 >> 8) & 0xFF; - BYTE[3] = addr2 & 0xFF; + static uint8_t byte[4]; + byte[0] = (addr1 >> 8) & 0xFF; + byte[1] = addr1 & 0xFF; + byte[2] = (addr2 >> 8) & 0xFF; + byte[3] = addr2 & 0xFF; this->dc_pin_->digital_write(true); - this->write_array(BYTE, 4); + this->write_array(byte, 4); } void ST7789V::write_color_(uint16_t color, uint16_t size) { - static uint8_t BYTE[1024]; + static uint8_t byte[1024]; int index = 0; for (int i = 0; i < size; i++) { - BYTE[index++] = (color >> 8) & 0xFF; - BYTE[index++] = color & 0xFF; + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; } this->dc_pin_->digital_write(true); - return write_array(BYTE, size * 2); + return write_array(byte, size * 2); } int ST7789V::get_height_internal() { diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 0e17e65fd7..2aef043ba0 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -19,13 +19,13 @@ static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod -static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Result static const uint8_t ST7789_SLPIN = 0x10; // Sleep in static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out -static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O -static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode On +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode On static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off -static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_INVON = 0x21; // Display Inversion On static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set static const uint8_t ST7789_DISPOFF = 0x28; // Display Off static const uint8_t ST7789_DISPON = 0x29; // Display On @@ -34,18 +34,18 @@ static const uint8_t ST7789_RASET = 0x2B; // Row Address Set static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read static const uint8_t ST7789_PTLAR = 0x30; // Partial Area -static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio -static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definition +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line Off static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off -static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_IDMON = 0x39; // Idle Mode On static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline -static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanline static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display @@ -59,17 +59,17 @@ static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control -static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Control static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) -static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro -static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro -static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Control +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Control +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustment static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control -static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin -static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Setting +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enable static const uint8_t ST7789_VRHS = 0xC3; // VRH Set static const uint8_t ST7789_VDVS = 0xC4; // VDV Set static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set @@ -89,8 +89,8 @@ static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control -static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro -static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Control +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enable static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting static const uint8_t ST7789_PROMACT = 0xFE; // Program action diff --git a/esphome/components/st7920/__init__.py b/esphome/components/st7920/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py new file mode 100644 index 0000000000..9b544fa644 --- /dev/null +++ b/esphome/components/st7920/display.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_WIDTH, CONF_HEIGHT + +AUTO_LOAD = ["display"] +CODEOWNERS = ["@marsjan155"] +DEPENDENCIES = ["spi"] + +st7920_ns = cg.esphome_ns.namespace("st7920") +ST7920 = st7920_ns.class_( + "ST7920", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice +) +ST7920Ref = ST7920.operator("ref") + +CONFIG_SCHEMA = ( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ST7920), + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } + ) + .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 CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(ST7920Ref, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + + await display.register_display(var, config) diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp new file mode 100644 index 0000000000..d985b0a426 --- /dev/null +++ b/esphome/components/st7920/st7920.cpp @@ -0,0 +1,146 @@ +#include "st7920.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7920 { + +static const char *const TAG = "st7920"; + +// ST7920 COMMANDS +static const uint8_t LCD_DATA = 0xFA; +static const uint8_t LCD_COMMAND = 0xF8; +static const uint8_t LCD_CLS = 0x01; +static const uint8_t LCD_HOME = 0x02; +static const uint8_t LCD_ADDRINC = 0x06; +static const uint8_t LCD_DISPLAYON = 0x0C; +static const uint8_t LCD_DISPLAYOFF = 0x08; +static const uint8_t LCD_CURSORON = 0x0E; +static const uint8_t LCD_CURSORBLINK = 0x0F; +static const uint8_t LCD_BASIC = 0x30; +static const uint8_t LCD_GFXMODE = 0x36; +static const uint8_t LCD_EXTEND = 0x34; +static const uint8_t LCD_TXTMODE = 0x34; +static const uint8_t LCD_STANDBY = 0x01; +static const uint8_t LCD_SCROLL = 0x03; +static const uint8_t LCD_SCROLLADDR = 0x40; +static const uint8_t LCD_ADDR = 0x80; +static const uint8_t LCD_LINE0 = 0x80; +static const uint8_t LCD_LINE1 = 0x90; +static const uint8_t LCD_LINE2 = 0x88; +static const uint8_t LCD_LINE3 = 0x98; + +void ST7920::setup() { + ESP_LOGCONFIG(TAG, "Setting up ST7920..."); + this->dump_config(); + this->spi_setup(); + this->init_internal_(this->get_buffer_length_()); + display_init_(); +} + +void ST7920::command_(uint8_t value) { + this->enable(); + this->send_(LCD_COMMAND, value); + this->disable(); +} + +void ST7920::data_(uint8_t value) { + this->enable(); + this->send_(LCD_DATA, value); + this->disable(); +} + +void ST7920::send_(uint8_t type, uint8_t value) { + this->write_byte(type); + this->write_byte(value & 0xF0); + this->write_byte(value << 4); +} + +void ST7920::goto_xy_(uint16_t x, uint16_t y) { + if (y >= 32 && y < 64) { + y -= 32; + x += 8; + } else if (y >= 64 && y < 64 + 32) { + y -= 32; + x += 0; + } else if (y >= 64 + 32 && y < 64 + 64) { + y -= 64; + x += 8; + } + this->command_(LCD_ADDR | y); // 6-bit (0..63) + this->command_(LCD_ADDR | x); // 4-bit (0..15) +} + +void HOT ST7920::write_display_data() { + uint8_t i, j, b; + for (j = 0; j < this->get_height_internal() / 2; j++) { + this->goto_xy_(0, j); + this->enable(); + for (i = 0; i < 16; i++) { // 16 bytes from line #0+ + b = this->buffer_[i + j * 16]; + this->send_(LCD_DATA, b); + } + for (i = 0; i < 16; i++) { // 16 bytes from line #32+ + b = this->buffer_[i + (j + 32) * 16]; + this->send_(LCD_DATA, b); + } + this->disable(); + App.feed_wdt(); + } +} + +void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } + +void ST7920::dump_config() { + LOG_DISPLAY("", "ST7920", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Height: %d", this->height_); + ESP_LOGCONFIG(TAG, " Width: %d", this->width_); +} + +float ST7920::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7920::update() { + this->clear(); + if (this->writer_local_.has_value()) // call lambda function if available + (*this->writer_local_)(*this); + this->write_display_data(); +} + +int ST7920::get_width_internal() { return this->width_; } + +int ST7920::get_height_internal() { return this->height_; } + +size_t ST7920::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT ST7920::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) { + ESP_LOGW(TAG, "Position out of area: %dx%d", x, y); + return; + } + int width = this->get_width_internal() / 8u; + if (color.is_on()) { + this->buffer_[y * width + x / 8] |= (0x80 >> (x & 7)); + } else { + this->buffer_[y * width + x / 8] &= ~(0x80 >> (x & 7)); + } +} + +void ST7920::display_init_() { + ESP_LOGD(TAG, "Initializing display..."); + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_CLS); // clear screen + delay(12); // >10 ms delay + this->command_(LCD_ADDRINC); // cursor increment right no shift + this->command_(LCD_DISPLAYON); // D=1, C=0, B=0 + this->command_(LCD_EXTEND); // LCD_EXTEND); + this->command_(LCD_GFXMODE); // LCD_GFXMODE); + this->write_display_data(); +} + +} // namespace st7920 +} // namespace esphome diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h new file mode 100644 index 0000000000..d0258d922c --- /dev/null +++ b/esphome/components/st7920/st7920.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace st7920 { + +class ST7920; + +using st7920_writer_t = std::function; + +class ST7920 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } + void set_height(uint16_t height) { this->height_ = height; } + void set_width(uint16_t width) { this->width_ = width; } + + // ========== 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; + void fill(Color color) override; + void write_display_data(); + + protected: + 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_(); + void display_init_(); + void command_(uint8_t value); + void data_(uint8_t value); + void send_(uint8_t type, uint8_t value); + void goto_xy_(uint16_t x, uint16_t y); + void start_transaction_(); + void end_transaction_(); + + int16_t width_ = 128, height_ = 64; + optional writer_local_{}; +}; + +} // namespace st7920 +} // namespace esphome diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 152e3aff9d..1795a9c41b 100644 --- a/esphome/components/status/status_binary_sensor.cpp +++ b/esphome/components/status/status_binary_sensor.cpp @@ -1,6 +1,6 @@ #include "status_binary_sensor.h" #include "esphome/core/log.h" -#include "esphome/core/util.h" +#include "esphome/components/network/util.h" #include "esphome/core/defines.h" #ifdef USE_MQTT @@ -16,7 +16,7 @@ namespace status { static const char *const TAG = "status"; void StatusBinarySensor::loop() { - bool status = network_is_connected(); + bool status = network::is_connected(); #ifdef USE_MQTT if (mqtt::global_mqtt_client != nullptr) { status = status && mqtt::global_mqtt_client->is_connected(); diff --git a/esphome/components/status_led/light/__init__.py b/esphome/components/status_led/light/__init__.py new file mode 100644 index 0000000000..8896046998 --- /dev/null +++ b/esphome/components/status_led/light/__init__.py @@ -0,0 +1,26 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light +from esphome.const import CONF_OUTPUT_ID, CONF_PIN +from .. import status_led_ns + +StatusLEDLightOutput = status_led_ns.class_( + "StatusLEDLightOutput", light.LightOutput, cg.Component +) + +CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(StatusLEDLightOutput), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + await cg.register_component(var, config) + # cg.add(cg.App.register_component(var)) + await light.register_light(var, config) diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp new file mode 100644 index 0000000000..760c89f972 --- /dev/null +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -0,0 +1,68 @@ +#include "status_led_light.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace status_led { + +static const char *const TAG = "status_led"; + +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); + } + + if ((new_state & STATUS_LED_ERROR) != 0u) { + this->pin_->digital_write(millis() % 250u < 150u); + this->last_app_state_ = new_state; + } else if ((new_state & STATUS_LED_WARNING) != 0u) { + this->pin_->digital_write(millis() % 1500u < 250u); + this->last_app_state_ = new_state; + } else if (new_state != this->last_app_state_) { + // if no error/warning -> restore light state or turn off + bool state = false; + + if (lightstate_) + lightstate_->current_values_as_binary(&state); + + this->pin_->digital_write(state); + this->last_app_state_ = new_state; + + ESP_LOGD(TAG, "Restoring light state %s", ONOFF(state)); + } +} + +void StatusLEDLightOutput::setup_state(light::LightState *state) { + lightstate_ = state; + ESP_LOGD(TAG, "'%s': Setting initital state", state->get_name().c_str()); + this->write_state(state); +} + +void StatusLEDLightOutput::write_state(light::LightState *state) { + bool binary; + state->current_values_as_binary(&binary); + + // if in warning/error, don't overwrite the status_led + // once it is back to OK, the loop will restore the state + if ((App.get_app_state() & (STATUS_LED_ERROR | STATUS_LED_WARNING)) == 0u) { + this->pin_->digital_write(binary); + ESP_LOGD(TAG, "'%s': Setting state %s", state->get_name().c_str(), ONOFF(binary)); + } +} + +void StatusLEDLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up Status LED..."); + + this->pin_->setup(); + this->pin_->digital_write(false); +} + +void StatusLEDLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Status Led Light:"); + LOG_PIN(" Pin: ", this->pin_); +} + +} // namespace status_led +} // namespace esphome diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h new file mode 100644 index 0000000000..e90d381e3c --- /dev/null +++ b/esphome/components/status_led/light/status_led_light.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace status_led { + +class StatusLEDLightOutput : public light::LightOutput, public Component { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + return traits; + } + + void loop() override; + + void setup_state(light::LightState *state) override; + + void write_state(light::LightState *state) override; + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_loop_priority() const override { return 50.0f; } + + protected: + GPIOPin *pin_; + light::LightState *lightstate_{}; + uint32_t last_app_state_{0xFFFF}; +}; + +} // namespace status_led +} // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 79f9f524a3..490557f3e7 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { namespace status_led { diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 41b1db7df2..54f6aa4205 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -21,6 +21,8 @@ Stepper = stepper_ns.class_("Stepper") SetTargetAction = stepper_ns.class_("SetTargetAction", automation.Action) ReportPositionAction = stepper_ns.class_("ReportPositionAction", automation.Action) SetSpeedAction = stepper_ns.class_("SetSpeedAction", automation.Action) +SetAccelerationAction = stepper_ns.class_("SetAccelerationAction", automation.Action) +SetDecelerationAction = stepper_ns.class_("SetDecelerationAction", automation.Action) def validate_acceleration(value): @@ -138,11 +140,47 @@ async def stepper_report_position_to_code(config, action_id, template_arg, args) async def stepper_set_speed_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_SPEED], args, cg.int32) + template_ = await cg.templatable(config[CONF_SPEED], args, cg.float_) cg.add(var.set_speed(template_)) return var +@automation.register_action( + "stepper.set_acceleration", + SetAccelerationAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_ACCELERATION): cv.templatable(validate_acceleration), + } + ), +) +async def stepper_set_acceleration_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_ACCELERATION], args, cg.float_) + cg.add(var.set_acceleration(template_)) + return var + + +@automation.register_action( + "stepper.set_deceleration", + SetDecelerationAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_DECELERATION): cv.templatable(validate_acceleration), + } + ), +) +async def stepper_set_deceleration_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_DECELERATION], args, cg.float_) + cg.add(var.set_deceleration(template_)) + return var + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(stepper_ns.using) diff --git a/esphome/components/stepper/stepper.cpp b/esphome/components/stepper/stepper.cpp index d7f6cc6dda..7926024204 100644 --- a/esphome/components/stepper/stepper.cpp +++ b/esphome/components/stepper/stepper.cpp @@ -1,5 +1,6 @@ #include "stepper.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace stepper { diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 33777dce83..560362e4d0 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -77,5 +77,35 @@ template class SetSpeedAction : public Action { Stepper *parent_; }; +template class SetAccelerationAction : public Action { + public: + explicit SetAccelerationAction(Stepper *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, acceleration); + + void play(Ts... x) override { + float acceleration = this->acceleration_.value(x...); + this->parent_->set_acceleration(acceleration); + } + + protected: + Stepper *parent_; +}; + +template class SetDecelerationAction : public Action { + public: + explicit SetDecelerationAction(Stepper *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, deceleration); + + void play(Ts... x) override { + float deceleration = this->deceleration_.value(x...); + this->parent_->set_deceleration(deceleration); + } + + protected: + Stepper *parent_; +}; + } // namespace stepper } // namespace esphome diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index 9de077c20a..b02c835ef8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ STS3XComponent = sts3x_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index 5d6d725a17..b1ecbc98f8 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -97,10 +97,9 @@ uint8_t sts3x_crc(uint8_t data1, uint8_t data2) { bool STS3XComponent::read_data_(uint16_t *data, uint8_t len) { const uint8_t num_bytes = len * 3; - auto *buf = new uint8_t[num_bytes]; + std::vector buf(num_bytes); - if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { - delete[](buf); + if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { return false; } @@ -109,13 +108,11 @@ bool STS3XComponent::read_data_(uint16_t *data, uint8_t len) { uint8_t crc = sts3x_crc(buf[j], buf[j + 1]); if (crc != buf[j + 2]) { ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - delete[](buf); return false; } data[i] = (buf[j] << 8) | buf[j + 1]; } - delete[](buf); return true; } diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index e7a5e24ee6..0cef417c15 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -25,8 +25,7 @@ def validate_substitution_key(value): for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - "Substitution must only consist of upper/lowercase characters, the underscore " - "and numbers. The character '{}' cannot be used".format(char) + f"Substitution must only consist of upper/lowercase characters, the underscore and numbers. The character '{char}' cannot be used" ) return value @@ -42,6 +41,7 @@ async def to_code(config): pass +# pylint: disable=consider-using-f-string VARIABLE_PROG = re.compile( "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) ) @@ -133,8 +133,7 @@ def do_substitution_pass(config, command_line_substitutions): with cv.prepend_path("substitutions"): if not isinstance(substitutions, dict): raise cv.Invalid( - "Substitutions must be a key to value mapping, got {}" - "".format(type(substitutions)) + f"Substitutions must be a key to value mapping, got {type(substitutions)}" ) replace_keys = [] diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index 644490ffc6..236acfadef 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_DEGREES, ICON_WEATHER_SUNSET, @@ -22,7 +21,10 @@ TYPES = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DEGREES, ICON_WEATHER_SUNSET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_WEATHER_SUNSET, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 4fd034ce7d..113c14d431 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -79,10 +79,8 @@ struct SunAtTime { num_t jde; num_t t; - SunAtTime(num_t jde) : jde(jde) { - // eq 25.1, p. 163; julian centuries from the epoch J2000.0 - t = (jde - 2451545) / 36525.0; - } + // eq 25.1, p. 163; julian centuries from the epoch J2000.0 + SunAtTime(num_t jde) : jde(jde), t((jde - 2451545) / 36525.0) {} num_t mean_obliquity() const { // eq. 22.2, p. 147; mean obliquity of the ecliptic diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 6a8364a5f0..0a2e6bcf97 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -80,7 +80,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parentedparent_->elevation(); - if (isnan(current)) + if (std::isnan(current)) return; bool crossed; @@ -90,7 +90,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parentedlast_elevation_ >= this->elevation_ && this->elevation_ > current; } - if (crossed && !isnan(this->last_elevation_)) { + if (crossed && !std::isnan(this->last_elevation_)) { this->trigger(); } this->last_elevation_ = current; diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 647041c19c..88341e0add 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -4,23 +4,21 @@ from esphome import automation from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import ( - CONF_ICON, CONF_ID, - CONF_INTERNAL, CONF_INVERTED, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_MQTT_ID, - CONF_NAME, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True switch_ns = cg.esphome_ns.namespace("switch_") -Switch = switch_ns.class_("Switch", cg.Nameable) +Switch = switch_ns.class_("Switch", cg.EntityBase) SwitchPtr = Switch.operator("ptr") ToggleAction = switch_ns.class_("ToggleAction", automation.Action) @@ -38,10 +36,9 @@ SwitchTurnOffTrigger = switch_ns.class_( icon = cv.icon -SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +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_ICON): icon, cv.Optional(CONF_INVERTED): cv.boolean, cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { @@ -58,11 +55,8 @@ SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( async def setup_switch_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) - if CONF_ICON in config: - cg.add(var.set_icon(config[CONF_ICON])) + await setup_entity(var, config) + if CONF_INVERTED in config: cg.add(var.set_inverted(config[CONF_INVERTED])) for conf in config.get(CONF_ON_TURN_ON, []): diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index c96f9a40d0..e4d20719e1 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -6,17 +6,9 @@ namespace switch_ { static const char *const TAG = "switch"; -std::string Switch::icon() { return ""; } -Switch::Switch(const std::string &name) : Nameable(name), state(false) {} +Switch::Switch(const std::string &name) : EntityBase(name), state(false) {} Switch::Switch() : Switch("") {} -std::string Switch::get_icon() { - if (this->icon_.has_value()) - return *this->icon_; - return this->icon(); -} - -void Switch::set_icon(const std::string &icon) { this->icon_ = icon; } void Switch::turn_on() { ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); @@ -30,7 +22,7 @@ void Switch::toggle() { this->write_state(this->inverted_ == this->state); } optional Switch::get_initial_state() { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 966119a29f..071393003a 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/preferences.h" #include "esphome/core/helpers.h" @@ -9,7 +10,7 @@ namespace switch_ { #define LOG_SWITCH(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + 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()); \ } \ @@ -26,7 +27,7 @@ namespace switch_ { * A switch is basically just a combination of a binary sensor (for reporting switch values) * and a write_state method that writes a state to the hardware. */ -class Switch : public Nameable { +class Switch : public EntityBase { public: explicit Switch(); explicit Switch(const std::string &name); @@ -70,12 +71,6 @@ class Switch : public Nameable { */ void set_inverted(bool inverted); - /// Set the icon for this switch. "" for no icon. - void set_icon(const std::string &icon); - - /// Get the icon for this switch. Using icon() if not manually set - std::string get_icon(); - /** Set callback for state changes. * * @param callback The void(bool) callback. @@ -104,18 +99,8 @@ class Switch : public Nameable { */ virtual void write_state(bool state) = 0; - /** Override this to set the Home Assistant icon for this switch. - * - * Return "" to disable this feature. - * - * @return The icon of this switch, for example "mdi:fan". - */ - virtual std::string icon(); // NOLINT - uint32_t hash_base() override; - optional icon_{}; ///< The icon shown here. Not set means use default from switch. Empty means no icon. - CallbackManager state_callback_{}; bool inverted_{false}; Deduplicator publish_dedup_; diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 8e1239924a..f1b7d5f424 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -2,7 +2,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import i2c -from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, + CONF_PULLUP, +) CONF_KEYPAD = "keypad" CONF_KEY_ROWS = "key_rows" @@ -10,17 +18,12 @@ CONF_KEY_COLUMNS = "key_columns" CONF_SLEEP_TIME = "sleep_time" CONF_SCAN_TIME = "scan_time" CONF_DEBOUNCE_TIME = "debounce_time" +CONF_SX1509_ID = "sx1509_id" DEPENDENCIES = ["i2c"] MULTI_CONF = True sx1509_ns = cg.esphome_ns.namespace("sx1509") -SX1509GPIOMode = sx1509_ns.enum("SX1509GPIOMode") -SX1509_GPIO_MODES = { - "INPUT": SX1509GPIOMode.SX1509_INPUT, - "INPUT_PULLUP": SX1509GPIOMode.SX1509_INPUT_PULLUP, - "OUTPUT": SX1509GPIOMode.SX1509_OUTPUT, -} SX1509Component = sx1509_ns.class_("SX1509Component", cg.Component, i2c.I2CDevice) SX1509GPIOPin = sx1509_ns.class_("SX1509GPIOPin", cg.GPIOPin) @@ -64,34 +67,43 @@ async def to_code(config): cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME])) -CONF_SX1509 = "sx1509" -CONF_SX1509_ID = "sx1509_id" +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") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value -SX1509_OUTPUT_PIN_SCHEMA = cv.Schema( + +CONF_SX1509 = "sx1509" +SX1509_PIN_SCHEMA = cv.All( { - cv.Required(CONF_SX1509): cv.use_id(SX1509Component), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum( - SX1509_GPIO_MODES, upper=True + cv.GenerateID(): cv.declare_id(SX1509Component), + cv.Required(CONF_SX1509): cv.use_id(SX1509GPIOPin), + 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, } ) -SX1509_INPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_SX1509): cv.use_id(SX1509Component), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum(SX1509_GPIO_MODES, upper=True), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) -@pins.PIN_SCHEMA_REGISTRY.register( - CONF_SX1509, (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA) -) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509, SX1509_PIN_SCHEMA) async def sx1509_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_SX1509]) - return SX1509GPIOPin.new( - parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED] - ) + 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/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp index c68f8f9ded..e9c401eeed 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.cpp +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -16,7 +16,8 @@ void SX1509FloatOutputChannel::write_state(float state) { void SX1509FloatOutputChannel::setup() { ESP_LOGD(TAG, "setup pin %d", this->pin_); - this->parent_->pin_mode(this->pin_, SX1509_ANALOG_OUTPUT); + this->parent_->pin_mode(this->pin_, gpio::FLAG_OUTPUT); + this->parent_->setup_led_driver(this->pin_); this->turn_off(); } diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 14d9ad7a61..9095dfeffa 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -18,13 +18,15 @@ void SX1509Component::setup() { this->write_byte(REG_RESET, 0x34); uint16_t data; - this->read_byte_16(REG_INTERRUPT_MASK_A, &data); - if (data == 0xFF00) { - clock_(INTERNAL_CLOCK_2MHZ); - } else { + if (!this->read_byte_16(REG_INTERRUPT_MASK_A, &data)) { this->mark_failed(); return; } + if (data != 0xFF00) { + this->mark_failed(); + return; + } + clock_(INTERNAL_CLOCK_2MHZ); delayMicroseconds(500); if (this->has_keypad_) this->setup_keypad_(); @@ -49,7 +51,8 @@ void SX1509Component::loop() { bool SX1509Component::digital_read(uint8_t pin) { if (this->ddr_mask_ & (1 << pin)) { uint16_t temp_reg_data; - this->read_byte_16(REG_DATA_B, &temp_reg_data); + if (!this->read_byte_16(REG_DATA_B, &temp_reg_data)) + return false; if (temp_reg_data & (1 << pin)) return true; } @@ -68,9 +71,9 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) { this->write_byte_16(REG_DATA_B, temp_reg_data); } else { // Otherwise the pin is an input, pull-up/down - uint16_t temp_pullup; + uint16_t temp_pullup = 0; this->read_byte_16(REG_PULL_UP_B, &temp_pullup); - uint16_t temp_pull_down; + uint16_t temp_pull_down = 0; this->read_byte_16(REG_PULL_DOWN_B, &temp_pull_down); if (bit_value) { @@ -89,25 +92,21 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) { } } -void SX1509Component::pin_mode(uint8_t pin, uint8_t mode) { +void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->read_byte_16(REG_DIR_B, &this->ddr_mask_); - if ((mode == SX1509_OUTPUT) || (mode == SX1509_ANALOG_OUTPUT)) + if (flags == gpio::FLAG_OUTPUT) this->ddr_mask_ &= ~(1 << pin); else this->ddr_mask_ |= (1 << pin); this->write_byte_16(REG_DIR_B, this->ddr_mask_); - if (mode == INPUT_PULLUP) - digital_write(pin, HIGH); - - if (mode == SX1509_ANALOG_OUTPUT) { - setup_led_driver_(pin); - } + if (flags & gpio::FLAG_PULLUP) + digital_write(pin, true); } -void SX1509Component::setup_led_driver_(uint8_t pin) { - uint16_t temp_word; - uint8_t temp_byte; +void SX1509Component::setup_led_driver(uint8_t pin) { + uint16_t temp_word = 0; + uint8_t temp_byte = 0; this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); temp_word |= (1 << pin); @@ -140,18 +139,18 @@ void SX1509Component::setup_led_driver_(uint8_t pin) { this->write_byte_16(REG_DATA_B, temp_word); } -void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_freq_out, byte osc_divider) { +void SX1509Component::clock_(uint8_t osc_source, uint8_t osc_pin_function, uint8_t osc_freq_out, uint8_t osc_divider) { osc_source = (osc_source & 0b11) << 5; // 2-bit value, bits 6:5 osc_pin_function = (osc_pin_function & 1) << 4; // 1-bit value bit 4 osc_freq_out = (osc_freq_out & 0b1111); // 4-bit value, bits 3:0 uint8_t reg_clock = osc_source | osc_pin_function | osc_freq_out; this->write_byte(REG_CLOCK, reg_clock); - osc_divider = constrain(osc_divider, 1, 7); + osc_divider = clamp(osc_divider, 1, 7u); this->clk_x_ = 2000000; osc_divider = (osc_divider & 0b111) << 4; // 3-bit value, bits 6:4 - uint8_t reg_misc; + uint8_t reg_misc = 0; this->read_byte(REG_MISC, ®_misc); reg_misc &= ~(0b111 << 4); reg_misc |= osc_divider; @@ -159,7 +158,7 @@ void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_fr } void SX1509Component::setup_keypad_() { - uint8_t temp_byte; + uint8_t temp_byte = 0; // setup row/col pins for INPUT OUTPUT this->read_byte_16(REG_DIR_B, &this->ddr_mask_); @@ -199,14 +198,14 @@ void SX1509Component::setup_keypad_() { } uint16_t SX1509Component::read_key_data() { - uint16_t key_data; + uint16_t key_data = 0; this->read_byte_16(REG_KEY_DATA_1, &key_data); return (0xFFFF ^ key_data); } void SX1509Component::set_debounce_config_(uint8_t config_value) { // First make sure clock is configured - uint8_t temp_byte; + uint8_t temp_byte = 0; this->read_byte(REG_MISC, &temp_byte); temp_byte |= (1 << 4); // Just default to no divider if not set this->write_byte(REG_MISC, temp_byte); @@ -227,13 +226,13 @@ void SX1509Component::set_debounce_time_(uint8_t time) { break; } } - config_value = constrain(config_value, 0, 7); + config_value = clamp(config_value, 0, 7); set_debounce_config_(config_value); } void SX1509Component::set_debounce_enable_(uint8_t pin) { - uint16_t debounce_enable; + uint16_t debounce_enable = 0; this->read_byte_16(REG_DEBOUNCE_ENABLE_B, &debounce_enable); debounce_enable |= (1 << pin); this->write_byte_16(REG_DEBOUNCE_ENABLE_B, debounce_enable); diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 55d5e54091..5f0697b534 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -2,6 +2,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "sx1509_gpio_pin.h" #include "sx1509_registers.h" @@ -14,16 +15,6 @@ const uint8_t EXTERNAL_CLOCK = 1; const uint8_t SOFTWARE_RESET = 0; const uint8_t HARDWARE_RESET = 1; -const uint8_t ANALOG_OUTPUT = 0x03; // To set a pin mode for PWM output - -// PinModes for SX1509 pins -enum SX1509GPIOMode : uint8_t { - SX1509_INPUT = INPUT, // 0x00 - SX1509_INPUT_PULLUP = INPUT_PULLUP, // 0x02 - SX1509_ANALOG_OUTPUT = ANALOG_OUTPUT, // 0x03 - SX1509_OUTPUT = OUTPUT, // 0x01 -}; - const uint8_t REG_I_ON[16] = {REG_I_ON_0, REG_I_ON_1, REG_I_ON_2, REG_I_ON_3, REG_I_ON_4, REG_I_ON_5, REG_I_ON_6, REG_I_ON_7, REG_I_ON_8, REG_I_ON_9, REG_I_ON_10, REG_I_ON_11, REG_I_ON_12, REG_I_ON_13, REG_I_ON_14, REG_I_ON_15}; @@ -46,9 +37,9 @@ class SX1509Component : public Component, public i2c::I2CDevice { bool digital_read(uint8_t pin); uint16_t read_key_data(); void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; - void pin_mode(uint8_t pin, uint8_t mode); + void pin_mode(uint8_t pin, gpio::Flags flags); void digital_write(uint8_t pin, bool bit_value); - u_long get_clock() { return this->clk_x_; }; + uint32_t get_clock() { return this->clk_x_; }; void set_rows_cols(uint8_t rows, uint8_t cols) { this->rows_ = rows; this->cols_ = cols; @@ -59,10 +50,11 @@ class SX1509Component : public Component, public i2c::I2CDevice { void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; }; void register_keypad_binary_sensor(SX1509Processor *binary_sensor) { this->keypad_binary_sensors_.push_back(binary_sensor); - }; + } + void setup_led_driver(uint8_t pin); protected: - u_long clk_x_ = 2000000; + uint32_t clk_x_ = 2000000; uint8_t frequency_ = 0; uint16_t ddr_mask_ = 0x00; uint16_t input_mask_ = 0x00; @@ -81,7 +73,6 @@ class SX1509Component : public Component, public i2c::I2CDevice { void set_debounce_pin_(uint8_t pin); void set_debounce_enable_(uint8_t pin); void set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols); - void setup_led_driver_(uint8_t pin); void clock_(uint8_t osc_source = 2, uint8_t osc_pin_function = 1, uint8_t osc_freq_out = 0, uint8_t osc_divider = 0); }; diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index ac55ac1ca5..2c6e0b0c32 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -7,14 +7,15 @@ namespace sx1509 { static const char *const TAG = "sx1509_gpio_pin"; -void SX1509GPIOPin::setup() { - ESP_LOGD(TAG, "setup pin %d", this->pin_); - this->parent_->pin_mode(this->pin_, this->mode_); -} - -void SX1509GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +void SX1509GPIOPin::setup() { pin_mode(flags_); } +void SX1509GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string SX1509GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_); + return buffer; +} } // namespace sx1509 } // namespace esphome diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h index 39f841a2a4..4d8aa5ec83 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.h +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -9,15 +9,22 @@ class SX1509Component; class SX1509GPIOPin : public GPIOPin { public: - SX1509GPIOPin(SX1509Component *parent, uint8_t pin, uint8_t mode, bool inverted = false) - : GPIOPin(pin, mode, inverted), parent_(parent){}; void setup() override; - void pin_mode(uint8_t mode) 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(SX1509Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } protected: SX1509Component *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; }; } // namespace sx1509 diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index d73f397f16..b97b85993f 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -9,7 +9,7 @@ Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the SX1509 provides - input/output setting, writing pins high/low, reading the input value of pins, LED driver utilities (blink, breath, pwm), and -keypad engine utilites. +keypad engine utilities. Development environment specifics: IDE: Arduino 1.6.5 diff --git a/esphome/components/t6615/__init__.py b/esphome/components/t6615/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py new file mode 100644 index 0000000000..71a099d635 --- /dev/null +++ b/esphome/components/t6615/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@tylermenezes"] +DEPENDENCIES = ["uart"] + +t6615_ns = cg.esphome_ns.namespace("t6615") +T6615Component = t6615_ns.class_("T6615Component", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(T6615Component), + cv.Required(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, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "t6615", baud_rate=19200, require_rx=True, require_tx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp new file mode 100644 index 0000000000..c139c56ce4 --- /dev/null +++ b/esphome/components/t6615/t6615.cpp @@ -0,0 +1,95 @@ +#include "t6615.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace t6615 { + +static const char *const TAG = "t6615"; + +static const uint32_t T6615_TIMEOUT = 1000; +static const uint8_t T6615_MAGIC = 0xFF; +static const uint8_t T6615_ADDR_HOST = 0xFA; +static const uint8_t T6615_ADDR_SENSOR = 0xFE; +static const uint8_t T6615_COMMAND_GET_PPM[] = {0x02, 0x03}; +static const uint8_t T6615_COMMAND_GET_SERIAL[] = {0x02, 0x01}; +static const uint8_t T6615_COMMAND_GET_VERSION[] = {0x02, 0x0D}; +static const uint8_t T6615_COMMAND_GET_ELEVATION[] = {0x02, 0x0F}; +static const uint8_t T6615_COMMAND_GET_ABC[] = {0xB7, 0x00}; +static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01}; +static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02}; +static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; + +void T6615Component::send_ppm_command_() { + this->command_time_ = millis(); + this->command_ = T6615Command::GET_PPM; + this->write_byte(T6615_MAGIC); + this->write_byte(T6615_ADDR_SENSOR); + this->write_byte(sizeof(T6615_COMMAND_GET_PPM)); + this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM)); +} + +void T6615Component::loop() { + if (this->available() < 5) { + if (this->command_ == T6615Command::GET_PPM && millis() - this->command_time_ > T6615_TIMEOUT) { + /* command got eaten, clear the buffer and fire another */ + while (this->available()) + this->read(); + this->send_ppm_command_(); + } + return; + } + + uint8_t response_buffer[6]; + + /* by the time we get here, we know we have at least five bytes in the buffer */ + this->read_array(response_buffer, 5); + + // Read header + if (response_buffer[0] != T6615_MAGIC || response_buffer[1] != T6615_ADDR_HOST) { + ESP_LOGW(TAG, "Got bad data from T6615! Magic was %02X and address was %02X", response_buffer[0], + response_buffer[1]); + /* make sure the buffer is empty */ + while (this->available()) + this->read(); + /* try again to read the sensor */ + this->send_ppm_command_(); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + switch (this->command_) { + case T6615Command::GET_PPM: { + const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]); + ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); + this->co2_sensor_->publish_state(ppm); + break; + } + default: + break; + } + this->command_time_ = 0; + this->command_ = T6615Command::NONE; +} + +void T6615Component::update() { this->query_ppm_(); } + +void T6615Component::query_ppm_() { + if (this->co2_sensor_ == nullptr || + (this->command_ != T6615Command::NONE && millis() - this->command_time_ < T6615_TIMEOUT)) { + return; + } + + this->send_ppm_command_(); +} + +float T6615Component::get_setup_priority() const { return setup_priority::DATA; } +void T6615Component::dump_config() { + ESP_LOGCONFIG(TAG, "T6615:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(19200); +} + +} // namespace t6615 +} // namespace esphome diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h new file mode 100644 index 0000000000..fb53032e8d --- /dev/null +++ b/esphome/components/t6615/t6615.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace t6615 { + +enum class T6615Command : uint8_t { + NONE = 0, + GET_PPM, + GET_SERIAL, + GET_VERSION, + GET_ELEVATION, + GET_ABC, + ENABLE_ABC, + DISABLE_ABC, + SET_ELEVATION, +}; + +class T6615Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void loop() override; + void update() override; + void dump_config() override; + + void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; } + + protected: + void query_ppm_(); + void send_ppm_command_(); + + T6615Command command_ = T6615Command::NONE; + uint32_t command_time_ = 0; + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace t6615 +} // namespace esphome diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py index 62cbace56a..0f222b8fc7 100644 --- a/esphome/components/tca9548a/__init__.py +++ b/esphome/components/tca9548a/__init__.py @@ -1,30 +1,43 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c -from esphome.const import CONF_ID, CONF_SCAN +from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID, CONF_SCAN CODEOWNERS = ["@andreashergert1984"] DEPENDENCIES = ["i2c"] tca9548a_ns = cg.esphome_ns.namespace("tca9548a") -TCA9548AComponent = tca9548a_ns.class_( - "TCA9548AComponent", cg.PollingComponent, i2c.I2CMultiplexer -) +TCA9548AComponent = tca9548a_ns.class_("TCA9548AComponent", cg.Component, i2c.I2CDevice) +TCA9548AChannel = tca9548a_ns.class_("TCA9548AChannel", i2c.I2CBus) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(TCA9548AComponent), - cv.Optional(CONF_SCAN, default=True): cv.boolean, - } -).extend(i2c.i2c_device_schema(0x70)) +CONF_BUS_ID = "bus_id" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCA9548AComponent), + cv.Optional(CONF_SCAN): cv.invalid("This option has been removed"), + cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list( + { + cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } + ), + } + ) + .extend(i2c.i2c_device_schema(0x70)) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - cg.add_define("USE_I2C_MULTIPLEXER") await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - cg.add(var.set_scan(config[CONF_SCAN])) + + for conf in config[CONF_CHANNELS]: + chan = cg.new_Pvariable(conf[CONF_BUS_ID]) + cg.add(chan.set_parent(var)) + cg.add(chan.set_channel(conf[CONF_CHANNEL])) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index 472b8b6673..5117ad8969 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -6,35 +6,46 @@ 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_); + if (err != i2c::ERROR_OK) + return err; + return parent_->bus_->readv(address, buffers, cnt); +} +i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) { + auto err = parent_->switch_to_channel(channel_); + if (err != i2c::ERROR_OK) + return err; + return parent_->bus_->writev(address, buffers, cnt); +} + void TCA9548AComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); uint8_t status = 0; - if (!this->read_byte(0x00, &status)) { + if (!this->read_register(0x00, &status, 1)) { ESP_LOGI(TAG, "TCA9548A failed"); + this->mark_failed(); return; } - // out of range to make sure on first set_channel a new one will be set - this->current_channelno_ = 8; - ESP_LOGCONFIG(TAG, "Channels currently open: %d", status); + ESP_LOGD(TAG, "Channels currently open: %d", status); } void TCA9548AComponent::dump_config() { ESP_LOGCONFIG(TAG, "TCA9548A:"); LOG_I2C_DEVICE(this); - if (this->scan_) { - for (uint8_t i = 0; i < 8; i++) { - ESP_LOGCONFIG(TAG, "Activating channel: %d", i); - this->set_channel(i); - this->parent_->dump_config(); - } - } } -void TCA9548AComponent::set_channel(uint8_t channelno) { - if (this->current_channelno_ != channelno) { - this->current_channelno_ = channelno; - uint8_t channelbyte = 1 << channelno; - this->write_byte(0x70, channelbyte); +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_register(0x70, &channel_val, 1); + if (err == i2c::ERROR_OK) { + current_channel_ = channel; } + return err; } } // namespace tca9548a diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 50b1eb8b56..314346d317 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -6,17 +6,31 @@ namespace esphome { namespace tca9548a { -class TCA9548AComponent : public Component, public i2c::I2CMultiplexer { +class TCA9548AComponent; +class TCA9548AChannel : public i2c::I2CBus { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + void set_parent(TCA9548AComponent *parent) { parent_ = parent; } + + i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; + i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) override; + + protected: + uint8_t channel_; + TCA9548AComponent *parent_; +}; + +class TCA9548AComponent : public Component, public i2c::I2CDevice { public: - void set_scan(bool scan) { scan_ = scan; } void setup() override; void dump_config() override; void update(); - void set_channel(uint8_t channelno) override; + + i2c::ErrorCode switch_to_channel(uint8_t channel); protected: - bool scan_; - uint8_t current_channelno_; + friend class TCA9548AChannel; + uint8_t current_channel_ = 255; }; } // namespace tca9548a } // namespace esphome diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index da44b3c827..5b938ba0c3 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -47,7 +47,7 @@ void Tcl112Climate::transmit_state() { // Set mode switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[6] &= 0xF0; remote_state[6] |= TCL112_AUTO; break; @@ -204,7 +204,7 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; case TCL112_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index d0fa0c1732..6c74c86faf 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -7,9 +7,7 @@ from esphome.const import ( CONF_ID, CONF_ILLUMINANCE, CONF_INTEGRATION_TIME, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -49,13 +47,22 @@ TCS34725_GAINS = { } color_channel_schema = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) color_temperature_schema = sensor.sensor_schema( - UNIT_KELVIN, ICON_THERMOMETER, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KELVIN, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) illuminance_schema = sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 52548262c1..564d3dcda7 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -1,5 +1,6 @@ #include "tcs34725.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace tcs34725 { diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index d7bf8999ef..9a5712e10f 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index ffdb1509be..e7cc2fcb1b 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -9,7 +9,9 @@ CONF_TAG_NAME = "tag_name" TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( +CONFIG_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 +).extend( { cv.GenerateID(): cv.declare_id(TeleInfoSensor), cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), @@ -18,9 +20,9 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index fa76dcfe76..badd66ae83 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -7,7 +7,7 @@ namespace teleinfo { static const char *const TAG = "teleinfo"; /* Helpers */ -static int get_field(char *dest, char *buf_start, char *buf_end, int sep) { +static int get_field(char *dest, char *buf_start, char *buf_end, int sep, int max_len) { char *field_end; int len; @@ -15,6 +15,8 @@ static int get_field(char *dest, char *buf_start, char *buf_end, int sep) { if (!field_end) return 0; len = field_end - buf_start; + if (len >= max_len) + return len; strncpy(dest, buf_start, len); dest[len] = '\0'; @@ -106,9 +108,22 @@ void TeleInfo::loop() { * 0xa | Tag | 0x9 | Data | 0x9 | CRC | 0xd * ^^^^^^^^^^^^^^^^^^^^^^^^^ * Checksum is computed on the above in standard mode. + * + * Note that some Tags may have a timestamp in Standard mode. In this case + * the group would looks like this: + * 0xa | Tag | 0x9 | Timestamp | 0x9 | Data | 0x9 | CRC | 0xd + * + * The DATE tag is a special case. The group looks like this + * 0xa | Tag | 0x9 | Timestamp | 0x9 | 0x9 | CRC | 0xd + * */ while ((buf_finger = static_cast(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) && ((buf_finger - buf_) < buf_index_)) { + /* + * Make sure timesamp is nullified between each tag as some tags don't + * have a timestamp + */ + timestamp_[0] = '\0'; /* Point to the first char of the group after 0xa */ buf_finger += 1; @@ -120,10 +135,10 @@ void TeleInfo::loop() { } if (!check_crc_(buf_finger, grp_end)) - break; + continue; /* Get tag */ - field_len = get_field(tag_, buf_finger, grp_end, separator_); + field_len = get_field(tag_, buf_finger, grp_end, separator_, MAX_TAG_SIZE); if (!field_len || field_len >= MAX_TAG_SIZE) { ESP_LOGE(TAG, "Invalid tag."); break; @@ -132,8 +147,22 @@ void TeleInfo::loop() { /* Advance buf_finger to after the tag and the separator. */ buf_finger += field_len + 1; - /* Get value (after next separator) */ - field_len = get_field(val_, buf_finger, grp_end, separator_); + /* + * If there is two separators and the tag is not equal to "DATE", + * it means there is a timestamp to read first. + */ + if (std::count(buf_finger, grp_end, separator_) == 2 && strcmp(tag_, "DATE") != 0) { + field_len = get_field(timestamp_, buf_finger, grp_end, separator_, MAX_TIMESTAMP_SIZE); + if (!field_len || field_len >= MAX_TIMESTAMP_SIZE) { + ESP_LOGE(TAG, "Invalid Timestamp"); + break; + } + + /* Advance buf_finger to after the first data and the separator. */ + buf_finger += field_len + 1; + } + + field_len = get_field(val_, buf_finger, grp_end, separator_, MAX_VAL_SIZE); if (!field_len || field_len >= MAX_VAL_SIZE) { ESP_LOGE(TAG, "Invalid Value"); break; diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h index f10024691e..2be34cfb78 100644 --- a/esphome/components/teleinfo/teleinfo.h +++ b/esphome/components/teleinfo/teleinfo.h @@ -11,7 +11,8 @@ namespace teleinfo { */ static const uint8_t MAX_TAG_SIZE = 64; static const uint16_t MAX_VAL_SIZE = 256; -static const uint16_t MAX_BUF_SIZE = 1024; +static const uint16_t MAX_BUF_SIZE = 2048; +static const uint16_t MAX_TIMESTAMP_SIZE = 14; class TeleInfoListener { public: @@ -36,6 +37,7 @@ class TeleInfo : public PollingComponent, public uart::UARTDevice { uint32_t buf_index_{0}; char tag_[MAX_TAG_SIZE]; char val_[MAX_VAL_SIZE]; + char timestamp_[MAX_TIMESTAMP_SIZE]; enum State { OFF, ON, diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index b1ade4df41..3bd73ff272 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -20,9 +20,9 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py new file mode 100644 index 0000000000..887f6b15ad --- /dev/null +++ b/esphome/components/template/number/__init__.py @@ -0,0 +1,91 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ID, + CONF_INITIAL_VALUE, + CONF_LAMBDA, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, + CONF_STEP, +) +from .. import template_ns + +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]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +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 CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + 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 number being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateNumber), + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.positive_float, + 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.float_, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate_min_max, + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_template(template_)) + + else: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if CONF_INITIAL_VALUE in config: + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] + ) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp new file mode 100644 index 0000000000..a5b015c44d --- /dev/null +++ b/esphome/components/template/number/template_number.cpp @@ -0,0 +1,55 @@ +#include "template_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.number"; + +void TemplateNumber::setup() { + if (this->f_.has_value()) + return; + + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) + value = this->initial_value_; + else + value = this->traits.get_min_value(); + } + } + this->publish_state(value); +} + +void TemplateNumber::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->publish_state(*val); +} + +void TemplateNumber::control(float value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} +void TemplateNumber::dump_config() { + LOG_NUMBER("", "Template Number", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h new file mode 100644 index 0000000000..9a82e44339 --- /dev/null +++ b/esphome/components/template/number/template_number.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateNumber : public number::Number, 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 set_trigger_; } + void set_optimistic(bool optimistic) { optimistic_ = optimistic; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + bool optimistic_{false}; + float initial_value_{NAN}; + bool restore_value_{false}; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py new file mode 100644 index 0000000000..4eba77119d --- /dev/null +++ b/esphome/components/template/select/__init__.py @@ -0,0 +1,84 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ID, + CONF_INITIAL_OPTION, + CONF_LAMBDA, + CONF_OPTIONS, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, +) +from .. import template_ns + +TemplateSelect = template_ns.class_( + "TemplateSelect", select.Select, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_OPTION 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") + elif CONF_INITIAL_OPTION in config: + if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: + raise cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + ) + else: + config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] + + 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 option being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSelect), + cv.Required(CONF_OPTIONS): cv.All( + cv.ensure_list(cv.string_strict), cv.Length(min=1) + ), + 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_OPTION): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await select.register_select(var, config, options=config[CONF_OPTIONS]) + + 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])) + cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + 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/select/template_select.cpp b/esphome/components/template/select/template_select.cpp new file mode 100644 index 0000000000..219c341ec9 --- /dev/null +++ b/esphome/components/template/select/template_select.cpp @@ -0,0 +1,74 @@ +#include "template_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.select"; + +void TemplateSelect::setup() { + if (this->f_.has_value()) + return; + + std::string value; + ESP_LOGD(TAG, "Setting up Template Select"); + if (!this->restore_value_) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial: %s", value.c_str()); + } else { + size_t index; + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + } else { + value = this->traits.get_options().at(index); + ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + } + } + + this->publish_state(value); +} + +void TemplateSelect::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + auto options = this->traits.get_options(); + if (std::find(options.begin(), options.end(), *val) == options.end()) { + ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + return; + } + + this->publish_state(*val); +} + +void TemplateSelect::control(const std::string &value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) { + auto options = this->traits.get_options(); + size_t index = std::find(options.begin(), options.end(), value) - options.begin(); + + this->pref_.save(&index); + } +} +void TemplateSelect::dump_config() { + LOG_SELECT("", "Template Select", this); + LOG_UPDATE_INTERVAL(this); + if (this->f_.has_value()) + return; + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + ESP_LOGCONFIG(TAG, " Initial Option: %s", this->initial_option_.c_str()); + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h new file mode 100644 index 0000000000..2f00765c3d --- /dev/null +++ b/esphome/components/template/select/template_select.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateSelect : public select::Select, 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_option(const std::string &initial_option) { this->initial_option_ = initial_option; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const std::string &value) override; + bool optimistic_ = false; + std::string initial_option_; + bool restore_value_ = false; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 47027583bf..75fb505d91 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -6,10 +6,7 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_STATE, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from .. import template_ns @@ -19,11 +16,8 @@ TemplateSensor = template_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 9324cb5dea..b28eb3fed2 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -1,5 +1,6 @@ #include "template_sensor.h" #include "esphome/core/log.h" +#include namespace esphome { namespace template_ { @@ -7,12 +8,13 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (!std::isnan(this->get_raw_state())) { + this->publish_state(this->get_raw_state()); } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index b00710dfb7..6095a7c561 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -16,17 +16,38 @@ from .. import template_ns TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateSwitch), - 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_TURN_OFF_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) + +def validate(config): + if ( + not config[CONF_OPTIMISTIC] + and CONF_TURN_ON_ACTION not in config + and CONF_TURN_OFF_ACTION not in config + ): + raise cv.Invalid( + "Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, " + "to handle the switch being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSwitch), + 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_TURN_OFF_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + validate, +) async def to_code(config): diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..83bebb5bcf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,12 +7,13 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (this->has_state()) { + this->publish_state(this->state); } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 84fedc8d94..5c739e1d0a 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -3,27 +3,34 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( - CONF_ICON, + CONF_FILTERS, CONF_ID, - CONF_INTERNAL, CONF_ON_VALUE, + CONF_ON_RAW_VALUE, CONF_TRIGGER_ID, CONF_MQTT_ID, - CONF_NAME, CONF_STATE, + CONF_FROM, + CONF_TO, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity +from esphome.util import Registry + IS_PLATFORM_COMPONENT = True # pylint: disable=invalid-name text_sensor_ns = cg.esphome_ns.namespace("text_sensor") -TextSensor = text_sensor_ns.class_("TextSensor", cg.Nameable) +TextSensor = text_sensor_ns.class_("TextSensor", cg.EntityBase) TextSensorPtr = TextSensor.operator("ptr") TextSensorStateTrigger = text_sensor_ns.class_( "TextSensorStateTrigger", automation.Trigger.template(cg.std_string) ) +TextSensorStateRawTrigger = text_sensor_ns.class_( + "TextSensorStateRawTrigger", automation.Trigger.template(cg.std_string) +) TextSensorPublishAction = text_sensor_ns.class_( "TextSensorPublishAction", automation.Action ) @@ -31,32 +38,115 @@ TextSensorStateCondition = text_sensor_ns.class_( "TextSensorStateCondition", automation.Condition ) +FILTER_REGISTRY = Registry() +validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) + +# Filters +Filter = text_sensor_ns.class_("Filter") +LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) +ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) +ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) +AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) +PrependFilter = text_sensor_ns.class_("PrependFilter", Filter) +SubstituteFilter = text_sensor_ns.class_("SubstituteFilter", Filter) + + +@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) +async def lambda_filter_to_code(config, filter_id): + lambda_ = await cg.process_lambda( + config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) + ) + return cg.new_Pvariable(filter_id, lambda_) + + +@FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) +async def to_upper_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id) + + +@FILTER_REGISTRY.register("to_lower", ToLowerFilter, {}) +async def to_lower_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id) + + +@FILTER_REGISTRY.register("append", AppendFilter, cv.string) +async def append_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) + + +@FILTER_REGISTRY.register("prepend", PrependFilter, cv.string) +async def prepend_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) + + +def validate_substitute(value): + if isinstance(value, dict): + return cv.Schema( + { + cv.Required(CONF_FROM): cv.string, + cv.Required(CONF_TO): cv.string, + } + )(value) + value = cv.string(value) + if "->" not in value: + raise cv.Invalid("Substitute mapping must contain '->'") + a, b = value.split("->", 1) + a, b = a.strip(), b.strip() + return validate_substitute({CONF_FROM: cv.string(a), CONF_TO: cv.string(b)}) + + +@FILTER_REGISTRY.register( + "substitute", + SubstituteFilter, + cv.All(cv.ensure_list(validate_substitute), cv.Length(min=2)), +) +async def substitute_filter_to_code(config, filter_id): + from_strings = [conf[CONF_FROM] for conf in config] + to_strings = [conf[CONF_TO] for conf in config] + return cg.new_Pvariable(filter_id, from_strings, to_strings) + + icon = cv.icon -TEXT_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +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.Optional(CONF_ICON): icon, + 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 + ), + } + ), } ) +async def build_filters(config): + return await cg.build_registry_list(FILTER_REGISTRY, config) + + async def setup_text_sensor_core_(var, config): - cg.add(var.set_name(config[CONF_NAME])) - if CONF_INTERNAL in config: - cg.add(var.set_internal(config[CONF_INTERNAL])) - if CONF_ICON in config: - cg.add(var.set_icon(config[CONF_ICON])) + await setup_entity(var, config) + + if config.get(CONF_FILTERS): # must exist and not be empty + filters = await build_filters(config[CONF_FILTERS]) + cg.add(var.set_filters(filters)) 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) + for conf in config.get(CONF_ON_RAW_VALUE, []): + 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) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index d82fd27c1f..d7286845e0 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -12,7 +12,14 @@ namespace text_sensor { class TextSensorStateTrigger : public Trigger { public: explicit TextSensorStateTrigger(TextSensor *parent) { - parent->add_on_state_callback([this](std::string value) { this->trigger(std::move(value)); }); + parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + } +}; + +class TextSensorStateRawTrigger : public Trigger { + public: + explicit TextSensorStateRawTrigger(TextSensor *parent) { + parent->add_on_raw_state_callback([this](const std::string &value) { this->trigger(value); }); } }; diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp new file mode 100644 index 0000000000..14df6238ff --- /dev/null +++ b/esphome/components/text_sensor/filter.cpp @@ -0,0 +1,74 @@ +#include "filter.h" +#include "text_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace text_sensor { + +static const char *const TAG = "text_sensor.filter"; + +// Filter +void Filter::input(const std::string &value) { + ESP_LOGVV(TAG, "Filter(%p)::input(%s)", this, value.c_str()); + optional out = this->new_value(value); + if (out.has_value()) + this->output(*out); +} +void Filter::output(const std::string &value) { + if (this->next_ == nullptr) { + ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> SENSOR", this, value.c_str()); + this->parent_->internal_send_state_to_frontend(value); + } else { + ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> %p", this, value.c_str(), this->next_); + this->next_->input(value); + } +} +void Filter::initialize(TextSensor *parent, Filter *next) { + ESP_LOGVV(TAG, "Filter(%p)::initialize(parent=%p next=%p)", this, parent, next); + this->parent_ = parent; + this->next_ = next; +} + +// LambdaFilter +LambdaFilter::LambdaFilter(lambda_filter_t lambda_filter) : lambda_filter_(std::move(lambda_filter)) {} +const lambda_filter_t &LambdaFilter::get_lambda_filter() const { return this->lambda_filter_; } +void LambdaFilter::set_lambda_filter(const lambda_filter_t &lambda_filter) { this->lambda_filter_ = lambda_filter; } + +optional LambdaFilter::new_value(std::string value) { + auto it = this->lambda_filter_(value); + ESP_LOGVV(TAG, "LambdaFilter(%p)::new_value(%s) -> %s", this, value.c_str(), it.value_or("").c_str()); + return it; +} + +// ToUpperFilter +optional ToUpperFilter::new_value(std::string value) { + for (char &c : value) + c = ::toupper(c); + return value; +} + +// ToLowerFilter +optional ToLowerFilter::new_value(std::string value) { + for (char &c : value) + c = ::toupper(c); + return value; +} + +// Append +optional AppendFilter::new_value(std::string value) { return value + this->suffix_; } + +// Prepend +optional PrependFilter::new_value(std::string value) { return this->prefix_ + value; } + +// Substitute +optional SubstituteFilter::new_value(std::string value) { + std::size_t pos; + for (int i = 0; i < this->from_strings_.size(); i++) + while ((pos = value.find(this->from_strings_[i])) != std::string::npos) + value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]); + return value; +} + +} // namespace text_sensor +} // namespace esphome diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h new file mode 100644 index 0000000000..6a1d9ab04e --- /dev/null +++ b/esphome/components/text_sensor/filter.h @@ -0,0 +1,112 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome { +namespace text_sensor { + +class TextSensor; + +/** Apply a filter to text sensor values such as to_upper. + * + * This class is purposefully kept quite simple, since more complicated + * filters should really be done with the filter sensor in Home Assistant. + */ +class Filter { + public: + /** This will be called every time the filter receives a new value. + * + * It can return an empty optional to indicate that the filter chain + * should stop, otherwise the value in the filter will be passed down + * the chain. + * + * @param value The new value. + * @return An optional string, the new value that should be pushed out. + */ + virtual optional new_value(std::string value); + + /// Initialize this filter, please note this can be called more than once. + virtual void initialize(TextSensor *parent, Filter *next); + + void input(const std::string &value); + + void output(const std::string &value); + + protected: + friend TextSensor; + + Filter *next_{nullptr}; + TextSensor *parent_{nullptr}; +}; + +using lambda_filter_t = std::function(std::string)>; + +/** This class allows for creation of simple template filters. + * + * The constructor accepts a lambda of the form std::string -> optional. + * It will be called with each new value in the filter chain and returns the modified + * value that shall be passed down the filter chain. Returning an empty Optional + * means that the value shall be discarded. + */ +class LambdaFilter : public Filter { + public: + explicit LambdaFilter(lambda_filter_t lambda_filter); + + optional new_value(std::string value) override; + + const lambda_filter_t &get_lambda_filter() const; + void set_lambda_filter(const lambda_filter_t &lambda_filter); + + protected: + lambda_filter_t lambda_filter_; +}; + +/// A simple filter that converts all text to uppercase +class ToUpperFilter : public Filter { + public: + optional new_value(std::string value) override; +}; + +/// A simple filter that converts all text to lowercase +class ToLowerFilter : public Filter { + public: + optional new_value(std::string value) override; +}; + +/// A simple filter that adds a string to the end of another string +class AppendFilter : public Filter { + public: + AppendFilter(std::string suffix) : suffix_(std::move(suffix)) {} + optional new_value(std::string value) override; + + protected: + std::string suffix_; +}; + +/// A simple filter that adds a string to the start of another string +class PrependFilter : public Filter { + public: + PrependFilter(std::string prefix) : prefix_(std::move(prefix)) {} + optional new_value(std::string value) override; + + protected: + std::string prefix_; +}; + +/// A simple filter that replaces a substring with another substring +class SubstituteFilter : public Filter { + public: + SubstituteFilter(std::vector from_strings, std::vector to_strings) + : from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {} + optional new_value(std::string value) override; + + protected: + std::vector from_strings_; + std::vector to_strings_; +}; + +} // namespace text_sensor +} // namespace esphome diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 8738860d55..0bcab90843 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -7,24 +7,67 @@ namespace text_sensor { static const char *const TAG = "text_sensor"; TextSensor::TextSensor() : TextSensor("") {} -TextSensor::TextSensor(const std::string &name) : Nameable(name) {} +TextSensor::TextSensor(const std::string &name) : EntityBase(name) {} void TextSensor::publish_state(const std::string &state) { - this->state = state; + this->raw_state = state; + this->raw_callback_.call(state); + + ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); + + if (this->filter_list_ == nullptr) { + this->internal_send_state_to_frontend(state); + } else { + this->filter_list_->input(state); + } +} + +void TextSensor::add_filter(Filter *filter) { + // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of + // filters + ESP_LOGVV(TAG, "TextSensor(%p)::add_filter(%p)", this, filter); + if (this->filter_list_ == nullptr) { + this->filter_list_ = filter; + } else { + Filter *last_filter = this->filter_list_; + while (last_filter->next_ != nullptr) + last_filter = last_filter->next_; + last_filter->initialize(this, filter); + } + filter->initialize(this, nullptr); +} +void TextSensor::add_filters(const std::vector &filters) { + for (Filter *filter : filters) { + this->add_filter(filter); + } +} +void TextSensor::set_filters(const std::vector &filters) { + this->clear_filters(); + this->add_filters(filters); +} +void TextSensor::clear_filters() { + if (this->filter_list_ != nullptr) { + ESP_LOGVV(TAG, "TextSensor(%p)::clear_filters()", this); + } + this->filter_list_ = nullptr; +} + +void TextSensor::add_on_state_callback(std::function callback) { + this->callback_.add(std::move(callback)); +} +void TextSensor::add_on_raw_state_callback(std::function callback) { + this->raw_callback_.add(std::move(callback)); +} + +std::string TextSensor::get_state() const { return this->state; } +std::string TextSensor::get_raw_state() const { return this->raw_state; } +void TextSensor::internal_send_state_to_frontend(const std::string &state) { + this->state = this->raw_state; this->has_state_ = true; ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); } -void TextSensor::set_icon(const std::string &icon) { this->icon_ = icon; } -void TextSensor::add_on_state_callback(std::function callback) { - this->callback_.add(std::move(callback)); -} -std::string TextSensor::get_icon() { - if (this->icon_.has_value()) - return *this->icon_; - return this->icon(); -} -std::string TextSensor::icon() { return ""; } + std::string TextSensor::unique_id() { return ""; } bool TextSensor::has_state() { return this->has_state_; } uint32_t TextSensor::hash_base() { return 334300109UL; } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 5c6e5be51a..4bd77131d7 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -1,14 +1,16 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/components/text_sensor/filter.h" namespace esphome { namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + 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()); \ } \ @@ -17,34 +19,53 @@ namespace text_sensor { } \ } -class TextSensor : public Nameable { +class TextSensor : public EntityBase { public: explicit TextSensor(); explicit TextSensor(const std::string &name); + /// Getter-syntax for .state. + std::string get_state() const; + /// Getter-syntax for .raw_state + std::string get_raw_state() const; + void publish_state(const std::string &state); - void set_icon(const std::string &icon); + /// Add a filter to the filter chain. Will be appended to the back. + void add_filter(Filter *filter); + + /// Add a list of vectors to the back of the filter chain. + void add_filters(const std::vector &filters); + + /// Clear the filters and replace them by filters. + void set_filters(const std::vector &filters); + + /// Clear the entire filter chain. + void clear_filters(); void add_on_state_callback(std::function callback); + /// Add a callback that will be called every time the sensor sends a raw value. + void add_on_raw_state_callback(std::function callback); std::string state; + std::string raw_state; // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - std::string get_icon(); - - virtual std::string icon(); - virtual std::string unique_id(); bool has_state(); + void internal_send_state_to_frontend(const std::string &state); + protected: uint32_t hash_base() override; - CallbackManager callback_; - optional icon_; + CallbackManager raw_callback_; ///< Storage for raw state callbacks. + CallbackManager callback_; ///< Storage for filtered state callbacks. + + Filter *filter_list_{nullptr}; ///< Store all active filters. + bool has_state_{false}; }; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 4a371ec165..7b5ee7c624 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -6,7 +6,10 @@ from esphome.const import ( CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, + CONF_COOL_DEADBAND, CONF_COOL_MODE, + CONF_COOL_OVERRUN, + CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, @@ -21,22 +24,45 @@ from esphome.const import ( CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, + CONF_FAN_ONLY_COOLING, CONF_FAN_ONLY_MODE, + CONF_FAN_WITH_COOLING, + CONF_FAN_WITH_HEATING, CONF_HEAT_ACTION, + CONF_HEAT_DEADBAND, CONF_HEAT_MODE, - CONF_HYSTERESIS, + CONF_HEAT_OVERRUN, CONF_ID, CONF_IDLE_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_FAN_MODE_SWITCHING_TIME, + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_MIN_IDLE_TIME, CONF_OFF_MODE, CONF_SENSOR, + CONF_SET_POINT_MINIMUM_DIFFERENTIAL, + CONF_STARTUP_DELAY, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION, + CONF_TARGET_TEMPERATURE_CHANGE_ACTION, ) CODEOWNERS = ["@kbx81"] +climate_ns = cg.esphome_ns.namespace("climate") thermostat_ns = cg.esphome_ns.namespace("thermostat") ThermostatClimate = thermostat_ns.class_( "ThermostatClimate", climate.Climate, cg.Component @@ -44,104 +70,241 @@ ThermostatClimate = thermostat_ns.class_( ThermostatClimateTargetTempConfig = thermostat_ns.struct( "ThermostatClimateTargetTempConfig" ) +ClimateMode = climate_ns.enum("ClimateMode") +CLIMATE_MODES = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) def validate_thermostat(config): - # verify corresponding climate action action exists for any defined climate mode action - if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE) - ) - if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE) - ) - if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format( - CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE - ) - ) - if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE) - ) - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): - raise cv.Invalid( - "{} must be defined when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, + # verify corresponding action(s) exist(s) for any defined climate mode or action + requirements = { + CONF_AUTO_MODE: [ + CONF_COOL_ACTION, + CONF_HEAT_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_MODE: [ + CONF_COOL_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_MODE: [ + CONF_DRY_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_FAN_ONLY_MODE: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_HEAT_MODE: [ + CONF_HEAT_ACTION, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_HEAT_ACTION: [ + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_SUPPLEMENTAL_COOLING_ACTION: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_SUPPLEMENTAL_HEATING_ACTION: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MAX_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_MAX_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MIN_COOLING_OFF_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_FANNING_OFF_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_FANNING_RUN_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_HEATING_OFF_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_MIN_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_SUPPLEMENTAL_COOLING_DELTA: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_ACTION, + ], + CONF_SUPPLEMENTAL_HEATING_DELTA: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_ACTION, + ], + } + for config_trigger, req_triggers in requirements.items(): + for req_trigger in req_triggers: + if config_trigger in config and req_trigger not in config: + raise cv.Invalid( + f"{req_trigger} must be defined to use {config_trigger}" + ) + + if CONF_FAN_ONLY_ACTION in config: + # determine validation requirements based on fan_only_action_uses_fan_mode_timer setting + if config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] is True: + requirements = [CONF_MIN_FAN_MODE_SWITCHING_TIME] + else: + requirements = [ + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + ] + for config_req_action in requirements: + if config_req_action not in config: + raise cv.Invalid( + f"{config_req_action} must be defined to use {CONF_FAN_ONLY_ACTION}" + ) + + # for any fan_mode action, confirm min_fan_mode_switching_time is defined + requirements = { + CONF_MIN_FAN_MODE_SWITCHING_TIME: [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_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, + ], + } + for req_config_item, config_triggers in requirements.items(): + for config_trigger in config_triggers: + if config_trigger in config and req_config_item not in config: + raise cv.Invalid( + f"{req_config_item} must be defined to use {config_trigger}" + ) + + # determine validation requirements based on fan_only_cooling setting + if config[CONF_FAN_ONLY_COOLING] is True: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [ CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION, - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: - raise cv.Invalid( - "{} must be defined when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) + ], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + else: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [CONF_COOL_ACTION], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in config and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in config and req_action not in config: + raise cv.Invalid(f"{config_temp} is defined with no {req_action}") if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in away and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined in away configuration when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in away and req_action not in config: + raise cv.Invalid( + f"{config_temp} is defined in away configuration with no {req_action}" + ) + + # verify default climate mode is valid given above configuration + default_mode = config[CONF_DEFAULT_MODE] + requirements = { + "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + "COOL": [CONF_COOL_ACTION], + "HEAT": [CONF_HEAT_ACTION], + "DRY": [CONF_DRY_ACTION], + "FAN_ONLY": [CONF_FAN_ONLY_ACTION], + "AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + }.get(default_mode, []) + for req in requirements: + if req not in config: raise cv.Invalid( - "{} must be defined in away configuration when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away - and CONF_HEAT_ACTION in config - ): - raise cv.Invalid( - "{} must be defined in away configuration when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away - and CONF_HEAT_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) + f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" + ) + if config[CONF_FAN_WITH_HEATING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_HEATING}" + ) + + # if min_fan_mode_switching_time is defined, at least one fan_mode action should be defined + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + requirements = [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_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, + ] + for config_req_action in requirements: + if config_req_action in config: + return config + raise cv.Invalid( + f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_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} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" + ) return config @@ -152,11 +315,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_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( + CONF_SUPPLEMENTAL_COOLING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation( single=True ), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional( + CONF_SUPPLEMENTAL_HEATING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), @@ -204,9 +373,42 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation( single=True ), + cv.Optional( + CONF_TARGET_TEMPERATURE_CHANGE_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( + validate_climate_mode + ), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Optional(CONF_HYSTERESIS, default=0.5): 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.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, + cv.Optional(CONF_MIN_COOLING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional( + CONF_MIN_FAN_MODE_SWITCHING_TIME + ): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_RUN_TIME): cv.positive_time_period_seconds, + 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_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, default=False + ): cv.boolean, + cv.Optional(CONF_FAN_ONLY_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_HEATING, default=False): cv.boolean, + cv.Optional(CONF_STARTUP_DELAY, default=False): cv.boolean, cv.Optional(CONF_AWAY_CONFIG): cv.Schema( { cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, @@ -227,14 +429,24 @@ async def to_code(config): await cg.register_component(var, config) await climate.register_climate(var, config) - auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config ) sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) + cg.add( + var.set_set_point_minimum_differential( + config[CONF_SET_POINT_MINIMUM_DIFFERENTIAL] + ) + ) cg.add(var.set_sensor(sens)) - cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + 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])) + cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) if two_points_available is True: cg.add(var.set_supports_two_points(True)) @@ -252,22 +464,94 @@ async def to_code(config): normal_config = ThermostatClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + + if CONF_MAX_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) + ) + + if CONF_MAX_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_maximum_run_time_in_sec(config[CONF_MAX_HEATING_RUN_TIME]) + ) + + if CONF_MIN_COOLING_OFF_TIME in config: + cg.add( + var.set_cooling_minimum_off_time_in_sec(config[CONF_MIN_COOLING_OFF_TIME]) + ) + + if CONF_MIN_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_minimum_run_time_in_sec(config[CONF_MIN_COOLING_RUN_TIME]) + ) + + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + cg.add( + var.set_fan_mode_minimum_switching_time_in_sec( + config[CONF_MIN_FAN_MODE_SWITCHING_TIME] + ) + ) + + if CONF_MIN_FANNING_OFF_TIME in config: + cg.add( + var.set_fanning_minimum_off_time_in_sec(config[CONF_MIN_FANNING_OFF_TIME]) + ) + + if CONF_MIN_FANNING_RUN_TIME in config: + cg.add( + var.set_fanning_minimum_run_time_in_sec(config[CONF_MIN_FANNING_RUN_TIME]) + ) + + if CONF_MIN_HEATING_OFF_TIME in config: + cg.add( + var.set_heating_minimum_off_time_in_sec(config[CONF_MIN_HEATING_OFF_TIME]) + ) + + if CONF_MIN_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_minimum_run_time_in_sec(config[CONF_MIN_HEATING_RUN_TIME]) + ) + + if CONF_SUPPLEMENTAL_COOLING_DELTA in config: + cg.add(var.set_supplemental_cool_delta(config[CONF_SUPPLEMENTAL_COOLING_DELTA])) + + if CONF_SUPPLEMENTAL_HEATING_DELTA in config: + cg.add(var.set_supplemental_heat_delta(config[CONF_SUPPLEMENTAL_HEATING_DELTA])) + + cg.add(var.set_idle_minimum_time_in_sec(config[CONF_MIN_IDLE_TIME])) + + cg.add( + var.set_supports_fan_only_action_uses_fan_mode_timer( + config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] + ) + ) + cg.add(var.set_supports_fan_only_cooling(config[CONF_FAN_ONLY_COOLING])) + cg.add(var.set_supports_fan_with_cooling(config[CONF_FAN_WITH_COOLING])) + cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) + + cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) cg.add(var.set_normal_config(normal_config)) await automation.build_automation( var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] ) - if auto_mode_available is True: - cg.add(var.set_supports_auto(True)) + if heat_cool_mode_available is True: + cg.add(var.set_supports_heat_cool(True)) else: - cg.add(var.set_supports_auto(False)) + cg.add(var.set_supports_heat_cool(False)) if CONF_COOL_ACTION in config: await automation.build_automation( var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] ) cg.add(var.set_supports_cool(True)) + if CONF_SUPPLEMENTAL_COOLING_ACTION in config: + await automation.build_automation( + var.get_supplemental_cool_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_COOLING_ACTION], + ) if CONF_DRY_ACTION in config: await automation.build_automation( var.get_dry_action_trigger(), [], config[CONF_DRY_ACTION] @@ -283,6 +567,12 @@ async def to_code(config): var.get_heat_action_trigger(), [], config[CONF_HEAT_ACTION] ) cg.add(var.set_supports_heat(True)) + if CONF_SUPPLEMENTAL_HEATING_ACTION in config: + await automation.build_automation( + var.get_supplemental_heat_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_HEATING_ACTION], + ) if CONF_AUTO_MODE in config: await automation.build_automation( var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] @@ -380,6 +670,12 @@ async def to_code(config): config[CONF_SWING_VERTICAL_ACTION], ) cg.add(var.set_supports_swing_mode_vertical(True)) + if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_temperature_change_trigger(), + [], + config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], + ) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 6154f293dc..ce15c53bbe 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -7,10 +7,20 @@ namespace thermostat { static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { + if (this->use_startup_delay_) { + // start timers so that no actions are called for a moment + this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::TIMER_HEATING_OFF); + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + } + // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; - // required action may have changed, recompute, refresh - this->switch_to_action_(compute_action_()); + // required action may have changed, recompute, refresh, we'll publish_state() later + this->switch_to_action_(this->compute_action_(), false); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); // current temperature and possibly action changed, so publish the new state this->publish_state(); }); @@ -21,192 +31,322 @@ void ThermostatClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles temps for us - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = this->default_mode_; this->change_away_(false); } - // refresh the climate action based on the restored settings - this->switch_to_action_(compute_action_()); + // refresh the climate action based on the restored settings, we'll publish_state() later + this->switch_to_action_(this->compute_action_(), false); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->setup_complete_ = true; this->publish_state(); } -float ThermostatClimate::hysteresis() { return this->hysteresis_; } + +float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } +float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } +float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } +float ThermostatClimate::heat_overrun() { return this->heating_overrun_; } + void ThermostatClimate::refresh() { - this->switch_to_mode_(this->mode); - this->switch_to_action_(compute_action_()); - this->switch_to_fan_mode_(this->fan_mode.value()); - this->switch_to_swing_mode_(this->swing_mode); + this->switch_to_mode_(this->mode, false); + this->switch_to_action_(this->compute_action_(), false); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); + this->switch_to_fan_mode_(this->fan_mode.value(), false); + this->switch_to_swing_mode_(this->swing_mode, false); + this->check_temperature_change_trigger_(); this->publish_state(); } + +bool ThermostatClimate::climate_action_change_delayed() { + bool state_mismatch = this->action != this->compute_action_(true); + + switch (this->compute_action_(true)) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + return state_mismatch && (!this->idle_action_ready_()); + case climate::CLIMATE_ACTION_COOLING: + return state_mismatch && (!this->cooling_action_ready_()); + case climate::CLIMATE_ACTION_HEATING: + return state_mismatch && (!this->heating_action_ready_()); + case climate::CLIMATE_ACTION_FAN: + return state_mismatch && (!this->fanning_action_ready_()); + case climate::CLIMATE_ACTION_DRYING: + return state_mismatch && (!this->drying_action_ready_()); + default: + break; + } + return false; +} + +bool ThermostatClimate::fan_mode_change_delayed() { + bool state_mismatch = this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != this->prev_fan_mode_; + return state_mismatch && (!this->fan_mode_ready_()); +} + +climate::ClimateAction ThermostatClimate::delayed_climate_action() { return this->compute_action_(true); } + +climate::ClimateFanMode ThermostatClimate::locked_fan_mode() { return this->prev_fan_mode_; } + +bool ThermostatClimate::hysteresis_valid() { + if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && + (std::isnan(this->cooling_deadband_) || std::isnan(this->cooling_overrun_))) + return false; + + if (this->supports_heat_ && (std::isnan(this->heating_deadband_) || std::isnan(this->heating_overrun_))) + return false; + + return true; +} + +void ThermostatClimate::validate_target_temperature() { + if (std::isnan(this->target_temperature)) { + this->target_temperature = + ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + + this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature must be between the visual minimum and the visual maximum + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } +} + +void ThermostatClimate::validate_target_temperatures() { + if (this->supports_two_points_) { + this->validate_target_temperature_low(); + this->validate_target_temperature_high(); + } else { + this->validate_target_temperature(); + } +} + +void ThermostatClimate::validate_target_temperature_low() { + if (std::isnan(this->target_temperature_low)) { + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature_low must not be lower than the visual minimum + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ + if (this->target_temperature_low > + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) + this->target_temperature_low = + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; + // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high + if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) + this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; + } +} + +void ThermostatClimate::validate_target_temperature_high() { + if (std::isnan(this->target_temperature_high)) { + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + } else { + // target_temperature_high must not be lower than the visual maximum + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ + if (this->target_temperature_high < + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) + this->target_temperature_high = + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; + // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low + if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) + this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; + } +} + void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_preset().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + } else { + this->preset = *call.get_preset(); + } + } if (call.get_mode().has_value()) this->mode = *call.get_mode(); if (call.get_fan_mode().has_value()) this->fan_mode = *call.get_fan_mode(); if (call.get_swing_mode().has_value()) this->swing_mode = *call.get_swing_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_target_temperature_low().has_value()) - this->target_temperature_low = *call.get_target_temperature_low(); - if (call.get_target_temperature_high().has_value()) - this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) { - // setup_complete_ blocks modifying/resetting the temps immediately after boot - if (this->setup_complete_) { - this->change_away_(*call.get_away()); - } else { - this->away = *call.get_away(); - } - } - // set point validation if (this->supports_two_points_) { - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - if (this->target_temperature_high < this->target_temperature_low) - this->target_temperature_high = this->target_temperature_low; + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + validate_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + validate_target_temperature_high(); + } } else { - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + validate_target_temperature(); + } } // make any changes happen refresh(); } + climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(this->supports_auto_); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_dry_mode(this->supports_dry_); - traits.set_supports_fan_only_mode(this->supports_fan_only_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); - traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); - traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); - traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); - traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); - traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); - traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); - traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); - traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); - traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); - traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); - traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); - traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + if (supports_auto_) + traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); + if (supports_heat_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_dry_) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (supports_fan_only_) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + + if (supports_fan_mode_on_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_ON); + if (supports_fan_mode_off_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_OFF); + if (supports_fan_mode_auto_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); + if (supports_fan_mode_low_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); + if (supports_fan_mode_medium_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); + if (supports_fan_mode_high_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); + if (supports_fan_mode_middle_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); + if (supports_fan_mode_focus_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); + if (supports_fan_mode_diffuse_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); + + if (supports_swing_mode_both_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + if (supports_swing_mode_horizontal_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + if (supports_swing_mode_off_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); + if (supports_swing_mode_vertical_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); + + if (supports_away_) + traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY}); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); - traits.set_supports_away(this->supports_away_); traits.set_supports_action(true); return traits; } -climate::ClimateAction ThermostatClimate::compute_action_() { - climate::ClimateAction target_action = this->action; - if (this->supports_two_points_) { - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || - isnan(this->target_temperature_high) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_AUTO: - case climate::CLIMATE_MODE_COOL: - case climate::CLIMATE_MODE_HEAT: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature_low - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } - } else { - if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_COOL: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - case climate::CLIMATE_MODE_HEAT: - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } +climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_timers) { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) { + return climate::CLIMATE_ACTION_OFF; + } + // do not change the action if an "ON" timer is running + if ((!ignore_timers) && + (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || + timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { + return this->action; + } + + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->fanning_required_()) + target_action = climate::CLIMATE_ACTION_FAN; + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->cooling_required_() && this->heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + // do not abruptly switch actions. cycle through IDLE, first. we'll catch this at the next update. + if ((((this->action == climate::CLIMATE_ACTION_COOLING) || (this->action == climate::CLIMATE_ACTION_DRYING)) && + (target_action == climate::CLIMATE_ACTION_HEATING)) || + ((this->action == climate::CLIMATE_ACTION_HEATING) && + ((target_action == climate::CLIMATE_ACTION_COOLING) || (target_action == climate::CLIMATE_ACTION_DRYING)))) { + return climate::CLIMATE_ACTION_IDLE; } - // do not switch to an action that isn't enabled per the active climate mode - if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) - target_action = climate::CLIMATE_ACTION_IDLE; - if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) - target_action = climate::CLIMATE_ACTION_IDLE; return target_action; } -void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { + +climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) { + return climate::CLIMATE_ACTION_OFF; + } + + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->supplemental_cooling_required_() && this->supplemental_heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + + return target_action; +} + +void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->action) && this->setup_complete_) // already in target mode @@ -215,34 +355,83 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && this->setup_complete_) { - // switching from OFF to IDLE or vice-versa - // these only have visual difference. OFF means user manually disabled, - // IDLE means it's in auto mode but value is in target range. + // switching from OFF to IDLE or vice-versa -- this is only a visual difference. + // OFF means user manually disabled, IDLE means the temperature is in target range. this->action = action; + if (publish_state) + this->publish_state(); return; } - if (this->prev_action_trigger_ != nullptr) { - this->prev_action_trigger_->stop_action(); - this->prev_action_trigger_ = nullptr; - } - Trigger<> *trig = this->idle_action_trigger_; + bool action_ready = false; + Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - // trig = this->idle_action_trigger_; + if (this->idle_action_ready_()) { + this->start_timer_(thermostat::TIMER_IDLE_ON); + if (this->action == climate::CLIMATE_ACTION_COOLING) + this->start_timer_(thermostat::TIMER_COOLING_OFF); + if (this->action == climate::CLIMATE_ACTION_FAN) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_OFF); + } + if (this->action == climate::CLIMATE_ACTION_HEATING) + this->start_timer_(thermostat::TIMER_HEATING_OFF); + // trig = this->idle_action_trigger_; + ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + action_ready = true; + } break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_action_trigger_; + if (this->cooling_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (this->supports_fan_with_cooling_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->cool_action_trigger_; + ESP_LOGVV(TAG, "Switching to COOLING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_action_trigger_; + if (this->heating_action_ready_()) { + this->start_timer_(thermostat::TIMER_HEATING_ON); + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (this->supports_fan_with_heating_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->heat_action_trigger_; + ESP_LOGVV(TAG, "Switching to HEATING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_FAN: - trig = this->fan_only_action_trigger_; + if (this->fanning_action_ready_()) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->fan_only_action_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_DRYING: - trig = this->dry_action_trigger_; + if (this->drying_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->dry_action_trigger_; + ESP_LOGVV(TAG, "Switching to DRYING action"); + action_ready = true; + } break; default: // we cannot report an invalid mode back to HA (even if it asked for one) @@ -250,63 +439,148 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { action = climate::CLIMATE_ACTION_OFF; // trig = this->idle_action_trigger_; } - assert(trig != nullptr); - trig->trigger(); - this->action = action; - this->prev_action_trigger_ = trig; + + if (action_ready) { + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop_action(); + this->prev_action_trigger_ = nullptr; + } + this->action = action; + this->prev_action_trigger_ = trig; + assert(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"); + trig_fan->trigger(); + } + if (publish_state) + this->publish_state(); + } } -void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + +void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->supplemental_action_) && this->setup_complete_) + // already in target mode + return; + + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_COOLING: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_HEATING: + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + default: + return; + } + ESP_LOGVV(TAG, "Updating supplemental action..."); + this->supplemental_action_ = action; + this->trigger_supplemental_action_(); +} + +void ThermostatClimate::trigger_supplemental_action_() { + Trigger<> *trig = nullptr; + + switch (this->supplemental_action_) { + case climate::CLIMATE_ACTION_COOLING: + if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + } + trig = this->supplemental_cool_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental COOLING action"); + break; + case climate::CLIMATE_ACTION_HEATING: + if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + } + trig = this->supplemental_heat_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental HEATING action"); + break; + default: + break; + } + + if (trig != nullptr) { + assert(trig != nullptr); + trig->trigger(); + } +} + +void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) // already in target mode return; - if (this->prev_fan_mode_trigger_ != nullptr) { - this->prev_fan_mode_trigger_->stop_action(); - this->prev_fan_mode_trigger_ = nullptr; - } - Trigger<> *trig = this->fan_mode_auto_trigger_; - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; - break; - case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; - break; - case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; - break; - case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; - break; - case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; - break; - case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; - break; - case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; - break; - case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; - break; - case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; - } - assert(trig != nullptr); - trig->trigger(); this->fan_mode = fan_mode; - this->prev_fan_mode_ = fan_mode; - this->prev_fan_mode_trigger_ = trig; + if (publish_state) + this->publish_state(); + + if (this->fan_mode_ready_()) { + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ON mode"); + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_OFF mode"); + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_AUTO mode"); + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_LOW mode"); + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode"); + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_HIGH mode"); + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode"); + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode"); + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop_action(); + this->prev_fan_mode_trigger_ = nullptr; + } + this->start_timer_(thermostat::TIMER_FAN_MODE); + assert(trig != nullptr); + trig->trigger(); + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; + } } -void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { + +void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((mode == this->prev_mode_) && this->setup_complete_) // already in target mode @@ -321,7 +595,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { case climate::CLIMATE_MODE_OFF: trig = this->off_mode_trigger_; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: // trig = this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: @@ -339,7 +613,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value - mode = climate::CLIMATE_MODE_AUTO; + mode = climate::CLIMATE_MODE_HEAT_COOL; // trig = this->auto_mode_trigger_; } assert(trig != nullptr); @@ -347,8 +621,11 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; + if (publish_state) + this->publish_state(); } -void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + +void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) // already in target mode @@ -383,7 +660,247 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->swing_mode = swing_mode; this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; + if (publish_state) + this->publish_state(); } + +bool ThermostatClimate::idle_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); + } + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::cooling_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::drying_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } + +bool ThermostatClimate::fanning_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + } + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); +} + +bool ThermostatClimate::heating_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || + this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); +} + +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].active = true; + } +} + +bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) { + this->timer_[timer_index].active = false; + return this->cancel_timeout(this->timer_[timer_index].name); +} + +bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].active; +} + +uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].time; +} + +std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].func; +} + +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_()); +} + +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_()); +} + +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_()); +} + +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_()); +} + +void ThermostatClimate::check_temperature_change_trigger_() { + if (this->supports_two_points_) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((this->prev_target_temperature_low_ == this->target_temperature_low) && + (this->prev_target_temperature_high_ == this->target_temperature_high) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperatures so we can check them again later; the trigger will fire below + this->prev_target_temperature_low_ = this->target_temperature_low; + this->prev_target_temperature_high_ = this->target_temperature_high; + } + } else { + if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperature so we can check it again later; the trigger will fire below + this->prev_target_temperature_ = this->target_temperature; + } + } + // trigger the action + Trigger<> *trig = this->temperature_change_trigger_; + assert(trig != nullptr); + trig->trigger(); +} + +bool ThermostatClimate::cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_cool_) { + if (this->current_temperature > temperature + this->cooling_deadband_) { + // if the current temperature exceeds the target + deadband, cooling is required + return true; + } else if (this->current_temperature < temperature - this->cooling_overrun_) { + // if the current temperature is less than the target - overrun, cooling should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_COOLING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_COOL)); + } + } + return false; +} + +bool ThermostatClimate::fanning_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_fan_only_) { + if (this->supports_fan_only_cooling_) { + if (this->current_temperature > temperature + this->cooling_deadband_) { + // if the current temperature exceeds the target + deadband, fanning is required + return true; + } else if (this->current_temperature < temperature - this->cooling_overrun_) { + // if the current temperature is less than the target - overrun, fanning should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_FAN) && (this->mode == climate::CLIMATE_MODE_FAN_ONLY); + } + } else { + return true; + } + } + return false; +} + +bool ThermostatClimate::heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + + if (this->supports_heat_) { + if (this->current_temperature < temperature - this->heating_deadband_) { + // if the current temperature is below the target - deadband, heating is required + return true; + } else if (this->current_temperature > temperature + this->heating_overrun_) { + // if the current temperature is above the target + overrun, heating should stop + return false; + } else { + // if we get here, the current temperature is between target - deadband and target + overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_HEATING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_HEAT)); + } + } + return false; +} + +bool ThermostatClimate::supplemental_cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + // the component must supports_cool_ and the climate action must be climate::CLIMATE_ACTION_COOLING. then... + // supplemental cooling is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_cool_ && (this->action == climate::CLIMATE_ACTION_COOLING) && + (this->cooling_max_runtime_exceeded_ || + (this->current_temperature > temperature + this->supplemental_cool_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_COOLING)); +} + +bool ThermostatClimate::supplemental_heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + // the component must supports_heat_ and the climate action must be climate::CLIMATE_ACTION_HEATING. then... + // supplemental heating is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_heat_ && (this->action == climate::CLIMATE_ACTION_HEATING) && + (this->heating_max_runtime_exceeded_ || + (this->current_temperature < temperature - this->supplemental_heat_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); +} + void ThermostatClimate::change_away_(bool away) { if (!away) { if (this->supports_two_points_) { @@ -398,21 +915,26 @@ void ThermostatClimate::change_away_(bool away) { } else this->target_temperature = this->away_config_.default_temperature; } - this->away = away; + this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } + void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; } + void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { this->supports_away_ = true; this->away_config_ = away_config; } + ThermostatClimate::ThermostatClimate() : cool_action_trigger_(new Trigger<>()), + supplemental_cool_action_trigger_(new Trigger<>()), cool_mode_trigger_(new Trigger<>()), dry_action_trigger_(new Trigger<>()), dry_mode_trigger_(new Trigger<>()), heat_action_trigger_(new Trigger<>()), + supplemental_heat_action_trigger_(new Trigger<>()), heat_mode_trigger_(new Trigger<>()), auto_mode_trigger_(new Trigger<>()), idle_action_trigger_(new Trigger<>()), @@ -431,13 +953,81 @@ ThermostatClimate::ThermostatClimate() swing_mode_both_trigger_(new Trigger<>()), swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } + swing_mode_vertical_trigger_(new Trigger<>()), + temperature_change_trigger_(new Trigger<>()) {} + +void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } +void ThermostatClimate::set_set_point_minimum_differential(float differential) { + this->set_point_minimum_differential_ = differential; +} +void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; } +void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; } +void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; } +void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } +void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } +void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } +void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FAN_MODE].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_IDLE_ON].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_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; +} void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_fan_only_action_uses_fan_mode_timer( + bool supports_fan_only_action_uses_fan_mode_timer) { + this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer; +} +void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) { + this->supports_fan_only_cooling_ = supports_fan_only_cooling; +} +void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) { + this->supports_fan_with_cooling_ = supports_fan_with_cooling; +} +void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) { + this->supports_fan_with_heating_ = supports_fan_with_heating; +} void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { this->supports_fan_mode_on_ = supports_fan_mode_on; @@ -481,10 +1071,17 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod void ThermostatClimate::set_supports_two_points(bool supports_two_points) { this->supports_two_points_ = supports_two_points; } + Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { + return this->supplemental_cool_action_trigger_; +} Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() const { + return this->supplemental_heat_action_trigger_; +} Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } @@ -505,6 +1102,8 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this- Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } + void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); if (this->supports_heat_) { @@ -513,17 +1112,62 @@ void ThermostatClimate::dump_config() { else ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); } - if ((this->supports_cool_) || (this->supports_fan_only_)) { + if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) { if (this->supports_two_points_) ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); else ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); } - ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + if (this->supports_cool_) { + ESP_LOGCONFIG(TAG, " Cooling Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->cooling_deadband_); + 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", + 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); + } + if (this->supports_heat_) { + ESP_LOGCONFIG(TAG, " Heating Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->heating_deadband_); + 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", + 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); + } + 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); + } + 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_) { + ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", + 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, " 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_)); ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s", + YESNO(this->supports_fan_only_action_uses_fan_mode_timer_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_COOLING: %s", YESNO(this->supports_fan_only_cooling_)); + if (this->supports_cool_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); @@ -559,8 +1203,10 @@ void ThermostatClimate::dump_config() { } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) : default_temperature(default_temperature) {} + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 86a1007efa..8d3e926752 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -8,6 +8,26 @@ namespace esphome { namespace thermostat { +enum ThermostatClimateTimerIndex : size_t { + TIMER_COOLING_MAX_RUN_TIME = 0, + TIMER_COOLING_OFF = 1, + TIMER_COOLING_ON = 2, + TIMER_FAN_MODE = 3, + TIMER_FANNING_OFF = 4, + TIMER_FANNING_ON = 5, + TIMER_HEATING_MAX_RUN_TIME = 6, + TIMER_HEATING_OFF = 7, + TIMER_HEATING_ON = 8, + TIMER_IDLE_ON = 9, +}; + +struct ThermostatClimateTimer { + const std::string name; + bool active; + uint32_t time; + std::function func; +}; + struct ThermostatClimateTargetTempConfig { public: ThermostatClimateTargetTempConfig(); @@ -17,7 +37,10 @@ struct ThermostatClimateTargetTempConfig { float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; - float hysteresis{NAN}; + float cool_deadband_{NAN}; + float cool_overrun_{NAN}; + float heat_deadband_{NAN}; + float heat_overrun_{NAN}; }; class ThermostatClimate : public climate::Climate, public Component { @@ -26,12 +49,35 @@ class ThermostatClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_hysteresis(float hysteresis); + void set_default_mode(climate::ClimateMode default_mode); + void set_set_point_minimum_differential(float differential); + void set_cool_deadband(float deadband); + void set_cool_overrun(float overrun); + void set_heat_deadband(float deadband); + void set_heat_overrun(float overrun); + void set_supplemental_cool_delta(float delta); + void set_supplemental_heat_delta(float delta); + void set_cooling_maximum_run_time_in_sec(uint32_t time); + void set_heating_maximum_run_time_in_sec(uint32_t time); + void set_cooling_minimum_off_time_in_sec(uint32_t time); + void set_cooling_minimum_run_time_in_sec(uint32_t time); + void set_fan_mode_minimum_switching_time_in_sec(uint32_t time); + void set_fanning_minimum_off_time_in_sec(uint32_t time); + void set_fanning_minimum_run_time_in_sec(uint32_t time); + void set_heating_minimum_off_time_in_sec(uint32_t time); + 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_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); + void set_supports_heat_cool(bool supports_heat_cool); void set_supports_cool(bool supports_cool); void set_supports_dry(bool supports_dry); void set_supports_fan_only(bool supports_fan_only); + void set_supports_fan_only_action_uses_fan_mode_timer(bool fan_only_action_uses_fan_mode_timer); + void set_supports_fan_only_cooling(bool supports_fan_only_cooling); + void set_supports_fan_with_cooling(bool supports_fan_with_cooling); + void set_supports_fan_with_heating(bool supports_fan_with_heating); void set_supports_heat(bool supports_heat); void set_supports_fan_mode_on(bool supports_fan_mode_on); void set_supports_fan_mode_off(bool supports_fan_mode_off); @@ -52,9 +98,11 @@ class ThermostatClimate : public climate::Climate, public Component { void set_away_config(const ThermostatClimateTargetTempConfig &away_config); Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_supplemental_cool_action_trigger() const; Trigger<> *get_dry_action_trigger() const; Trigger<> *get_fan_only_action_trigger() const; Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_supplemental_heat_action_trigger() const; Trigger<> *get_idle_action_trigger() const; Trigger<> *get_auto_mode_trigger() const; Trigger<> *get_cool_mode_trigger() const; @@ -75,10 +123,27 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; - /// Get current hysteresis value - float hysteresis(); + Trigger<> *get_temperature_change_trigger() const; + /// Get current hysteresis values + float cool_deadband(); + float cool_overrun(); + float heat_deadband(); + float heat_overrun(); /// Call triggers based on updated climate states (modes/actions) void refresh(); + /// Returns true if a climate action/fan mode transition is being delayed + bool climate_action_change_delayed(); + bool fan_mode_change_delayed(); + /// Returns the climate action that is being delayed (check climate_action_change_delayed(), first!) + climate::ClimateAction delayed_climate_action(); + /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) + climate::ClimateFanMode locked_fan_mode(); + /// Set point and hysteresis validation + bool hysteresis_valid(); // returns true if valid + void validate_target_temperature(); + void validate_target_temperatures(); + void validate_target_temperature_low(); + void validate_target_temperature_high(); protected: /// Override control to change settings of the climate device. @@ -91,19 +156,59 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateTraits traits() override; /// Re-compute the required action of this climate controller. - climate::ClimateAction compute_action_(); + climate::ClimateAction compute_action_(bool ignore_timers = false); + climate::ClimateAction compute_supplemental_action_(); /// Switch the climate device to the given climate action. - void switch_to_action_(climate::ClimateAction action); + void switch_to_action_(climate::ClimateAction action, bool publish_state = true); + void switch_to_supplemental_action_(climate::ClimateAction action); + void trigger_supplemental_action_(); /// Switch the climate device to the given climate fan mode. - void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state = true); /// Switch the climate device to the given climate mode. - void switch_to_mode_(climate::ClimateMode mode); + void switch_to_mode_(climate::ClimateMode mode, bool publish_state = true); /// Switch the climate device to the given climate swing mode. - void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state = true); + + /// Check if the temperature change trigger should be called. + void check_temperature_change_trigger_(); + + /// Is the action ready to be called? Returns true if so + bool idle_action_ready_(); + bool cooling_action_ready_(); + bool drying_action_ready_(); + bool fan_mode_ready_(); + bool fanning_action_ready_(); + bool heating_action_ready_(); + + /// Start/cancel/get status of climate action timer + void start_timer_(ThermostatClimateTimerIndex timer_index); + bool cancel_timer_(ThermostatClimateTimerIndex timer_index); + bool timer_active_(ThermostatClimateTimerIndex timer_index); + uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index); + std::function timer_cbf_(ThermostatClimateTimerIndex timer_index); + + /// set_timeout() callbacks for various actions (see above) + void cooling_max_run_time_timer_callback_(); + void cooling_off_timer_callback_(); + void cooling_on_timer_callback_(); + void fan_mode_timer_callback_(); + void fanning_off_timer_callback_(); + void fanning_on_timer_callback_(); + void heating_max_run_time_timer_callback_(); + void heating_off_timer_callback_(); + void heating_on_timer_callback_(); + void idle_on_timer_callback_(); + + /// Check if cooling/fanning/heating actions are required; returns true if so + bool cooling_required_(); + bool fanning_required_(); + bool heating_required_(); + bool supplemental_cooling_required_(); + bool supplemental_heating_required_(); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -113,10 +218,18 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value for any given attribute means that the controller has no such action /// (for example a thermostat, where only heating and not-heating is possible). bool supports_auto_{false}; + bool supports_heat_cool_{false}; bool supports_cool_{false}; bool supports_dry_{false}; bool supports_fan_only_{false}; bool supports_heat_{false}; + /// Special flag -- enables fan_modes to share timer with fan_only climate action + bool supports_fan_only_action_uses_fan_mode_timer_{false}; + /// Special flag -- enables fan to be switched based on target_temperature_high + bool supports_fan_only_cooling_{false}; + /// Special flags -- enables fan_only action to be called with cooling/heating actions + bool supports_fan_with_cooling_{false}; + bool supports_fan_with_heating_{false}; /// Whether the controller supports turning on or off just the fan. /// @@ -159,12 +272,23 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such mode. bool supports_away_{false}; + /// Flags indicating if maximum allowable run time was exceeded + bool cooling_max_runtime_exceeded_{false}; + bool heating_max_runtime_exceeded_{false}; + + /// Used to start "off" delay timers at boot + bool use_startup_delay_{false}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action /// For example electric heat, where only heating (power on) and not-heating /// (power off) is possible. Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *supplemental_cool_action_trigger_{nullptr}; Trigger<> *cool_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to dry (dehumidification) mode. @@ -180,6 +304,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// For example window blinds, where only cooling (blinds closed) and not-cooling /// (blinds open) is possible. Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *supplemental_heat_action_trigger_{nullptr}; Trigger<> *heat_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to auto mode. @@ -240,6 +365,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the swing mode to "vertical". Trigger<> *swing_mode_vertical_trigger_{nullptr}; + /// The trigger to call when the target temperature(s) change(es). + Trigger<> *temperature_change_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -252,19 +380,51 @@ class ThermostatClimate : public climate::Climate, public Component { /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called + climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; + /// Temperature data for normal/home and away modes ThermostatClimateTargetTempConfig normal_config_{}; ThermostatClimateTargetTempConfig away_config_{}; - /// Hysteresis value used for computing climate actions - float hysteresis_{0}; - - /// setup_complete_ blocks modifying/resetting the temps immediately after boot - bool setup_complete_{false}; + /// 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)}}; }; } // namespace thermostat diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 4872c89f88..5c2155d764 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,10 +1,8 @@ -import bisect -import datetime import logging -import math -import string +from importlib import resources +from typing import Optional +from datetime import timezone -import pytz import tzlocal import esphome.codegen as cg @@ -44,118 +42,47 @@ ESPTime = time_ns.struct("ESPTime") TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) -def _tz_timedelta(td): - offset_hour = int(td.total_seconds() / (60 * 60)) - offset_minute = int(abs(td.total_seconds() / 60)) % 60 - offset_second = int(abs(td.total_seconds())) % 60 - if offset_hour == 0 and offset_minute == 0 and offset_second == 0: - return "0" - if offset_minute == 0 and offset_second == 0: - return f"{offset_hour}" - if offset_second == 0: - return f"{offset_hour}:{offset_minute}" - return f"{offset_hour}:{offset_minute}:{offset_second}" - - -# https://stackoverflow.com/a/16804556/8924614 -def _week_of_month(dt): - first_day = dt.replace(day=1) - dom = dt.day - adjusted_dom = dom + first_day.weekday() - return int(math.ceil(adjusted_dom / 7.0)) - - -def _tz_dst_str(dt): - td = datetime.timedelta(hours=dt.hour, minutes=dt.minute, seconds=dt.second) - return "M{}.{}.{}/{}".format( - dt.month, _week_of_month(dt), dt.isoweekday() % 7, _tz_timedelta(td) - ) - - -def _safe_tzname(tz, dt): - tzname = tz.tzname(dt) - # pytz does not always return valid tznames - # For example: 'Europe/Saratov' returns '+04' - # Work around it by using a generic name for the timezone - if not all(c in string.ascii_letters for c in tzname): - return "TZ" - return tzname - - -def _non_dst_tz(tz, dt): - tzname = _safe_tzname(tz, dt) - utcoffset = tz.utcoffset(dt) - _LOGGER.info( - "Detected timezone '%s' with UTC offset %s", tzname, _tz_timedelta(utcoffset) - ) - tzbase = "{}{}".format(tzname, _tz_timedelta(-1 * utcoffset)) - return tzbase - - -def convert_tz(pytz_obj): - tz = pytz_obj - - now = datetime.datetime.now() - first_january = datetime.datetime(year=now.year, month=1, day=1) - - if not isinstance(tz, pytz.tzinfo.DstTzInfo): - return _non_dst_tz(tz, first_january) - - # pylint: disable=protected-access - transition_times = tz._utc_transition_times - transition_info = tz._transition_info - idx = max(0, bisect.bisect_right(transition_times, now)) - if idx >= len(transition_times): - return _non_dst_tz(tz, now) - - idx1, idx2 = idx, idx + 1 - dstoffset1 = transition_info[idx1][1] - if dstoffset1 == datetime.timedelta(seconds=0): - # Normalize to 1 being DST on - idx1, idx2 = idx + 1, idx + 2 - - if idx2 >= len(transition_times): - return _non_dst_tz(tz, now) - - if transition_times[idx2].year > now.year + 1: - # Next transition is scheduled after this year - # Probably a scheduler timezone change. - return _non_dst_tz(tz, now) - - utcoffset_on, _, tzname_on = transition_info[idx1] - utcoffset_off, _, tzname_off = transition_info[idx2] - dst_begins_utc = transition_times[idx1] - dst_begins_local = dst_begins_utc + utcoffset_off - dst_ends_utc = transition_times[idx2] - dst_ends_local = dst_ends_utc + utcoffset_on - - tzbase = "{}{}".format(tzname_off, _tz_timedelta(-1 * utcoffset_off)) - - tzext = "{}{},{},{}".format( - tzname_on, - _tz_timedelta(-1 * utcoffset_on), - _tz_dst_str(dst_begins_local), - _tz_dst_str(dst_ends_local), - ) - _LOGGER.info( - "Detected timezone '%s' with UTC offset %s and daylight saving time from " - "%s to %s", - tzname_off, - _tz_timedelta(utcoffset_off), - dst_begins_local.strftime("%d %B %X"), - dst_ends_local.strftime("%d %B %X"), - ) - return tzbase + tzext - - -def detect_tz(): +def _load_tzdata(iana_key: str) -> Optional[bytes]: + # From https://tzdata.readthedocs.io/en/latest/#examples try: - tz = tzlocal.get_localzone() - except pytz.exceptions.UnknownTimeZoneError: - _LOGGER.warning("Could not auto-detect timezone. Using UTC...") - return "UTC" + package_loc, resource = iana_key.rsplit("/", 1) + except ValueError: + return None + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") - return convert_tz(tz) + try: + return resources.read_binary(package, resource) + except (FileNotFoundError, ModuleNotFoundError): + return None + + +def _extract_tz_string(tzfile: bytes) -> str: + try: + return tzfile.split(b"\n")[-2].decode() + except (IndexError, UnicodeDecodeError): + _LOGGER.error("Could not determine TZ string. Please report this issue.") + _LOGGER.error("tzfile contents: %s", tzfile, exc_info=True) + raise + + +def detect_tz() -> str: + localzone = tzlocal.get_localzone() + if localzone is timezone.utc: + return "UTC0" + if not hasattr(localzone, "key"): + raise cv.Invalid( + "Could not automatically determine timezone, please set timezone manually." + ) + iana_key = localzone.key + _LOGGER.info("Detected timezone '%s'", iana_key) + tzfile = _load_tzdata(iana_key) + if tzfile is None: + raise cv.Invalid( + "Could not automatically determine timezone, please set timezone manually." + ) + ret = _extract_tz_string(tzfile) + _LOGGER.debug(" -> TZ string %s", ret) + return ret def _parse_cron_int(value, special_mapping, message): @@ -176,9 +103,7 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): data = part.split("/") if len(data) > 2: raise cv.Invalid( - "Can't have more than two '/' in one time expression, got {}".format( - part - ) + f"Can't have more than two '/' in one time expression, got {part}" ) offset, repeat = data offset_n = 0 @@ -194,18 +119,14 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): except ValueError: # pylint: disable=raise-missing-from raise cv.Invalid( - "Repeat for '/' time expression must be an integer, got {}".format( - repeat - ) + f"Repeat for '/' time expression must be an integer, got {repeat}" ) return set(range(offset_n, max_value + 1, repeat_n)) if "-" in part: data = part.split("-") if len(data) > 2: raise cv.Invalid( - "Can't have more than two '-' in range time expression '{}'".format( - part - ) + f"Can't have more than two '-' in range time expression '{part}'" ) begin, end = data begin_n = _parse_cron_int( @@ -233,13 +154,11 @@ def cron_expression_validator(name, min_value, max_value, special_mapping=None): for v in value: if not isinstance(v, int): raise cv.Invalid( - "Expected integer for {} '{}', got {}".format(v, name, type(v)) + f"Expected integer for {v} '{name}', got {type(v)}" ) if v < min_value or v > max_value: raise cv.Invalid( - "{} {} is out of range (min={} max={}).".format( - name, v, min_value, max_value - ) + f"{name} {v} is out of range (min={min_value} max={max_value})." ) return list(sorted(value)) value = cv.string(value) @@ -295,8 +214,7 @@ def validate_cron_raw(value): value = value.split(" ") if len(value) != 6: raise cv.Invalid( - "Cron expression must consist of exactly 6 space-separated parts, " - "not {}".format(len(value)) + f"Cron expression must consist of exactly 6 space-separated parts, not {len(value)}" ) seconds, minutes, hours, days_of_month, months, days_of_week = value return { @@ -343,15 +261,15 @@ def validate_cron_keys(value): return cv.has_at_least_one_key(*CRON_KEYS)(value) -def validate_tz(value): +def validate_tz(value: str) -> str: value = cv.string_strict(value) - try: - pytz_obj = pytz.timezone(value) - except pytz.UnknownTimeZoneError: # pylint: disable=broad-except + tzfile = _load_tzdata(value) + if tzfile is None: + # Not a IANA key, probably a TZ string return value - return convert_tz(pytz_obj) + return _extract_tz_string(tzfile) TIME_SCHEMA = cv.Schema( diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 6d34459fea..7e16d7141f 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -1,5 +1,6 @@ #include "automation.h" #include "esphome/core/log.h" +#include namespace esphome { namespace time { @@ -22,7 +23,10 @@ void CronTrigger::loop() { return; if (this->last_check_.has_value()) { - if (*this->last_check_ >= time) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > 900) { + // 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; } @@ -40,9 +44,9 @@ void CronTrigger::loop() { 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 DayOfWeek=%u DayOfMonth=%u DayOfYear=%u Month=%u time=%ld", + ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u DayOfWeek=%u DayOfMonth=%u DayOfYear=%u Month=%u time=%" PRId64, time.second, time.minute, time.hour, time.day_of_week, time.day_of_month, time.day_of_year, time.month, - time.timestamp); + (int64_t) time.timestamp); } if (this->matches(time)) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index c2a93b5191..064e6f899c 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -1,7 +1,7 @@ #include "real_time_clock.h" #include "esphome/core/log.h" #include "lwip/opt.h" -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include "sys/time.h" #endif #include @@ -35,9 +35,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } auto time = this->now(); - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); } diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 92a25fe993..0c6fa6f3a0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -32,11 +32,8 @@ struct ESPTime { uint16_t year; /// daylight saving time flag bool is_dst; - union { - ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t timestamp; - }; + /// 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. diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 066004f014..522252e907 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -1,5 +1,6 @@ #include "time_based_cover.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace time_based { @@ -51,6 +52,7 @@ float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_position(true); + traits.set_supports_toggle(true); traits.set_is_assumed_state(this->assumed_state_); return traits; } @@ -59,6 +61,20 @@ void TimeBasedCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_IDLE); this->publish_state(); } + if (call.get_toggle().has_value()) { + if (this->current_operation != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + this->publish_state(); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } if (call.get_position().has_value()) { auto pos = *call.get_position(); if (pos == this->position) { @@ -104,9 +120,11 @@ void TimeBasedCover::start_direction_(CoverOperation dir) { trig = this->stop_trigger_; break; case COVER_OPERATION_OPENING: + this->last_operation_ = dir; trig = this->open_trigger_; break; case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; trig = this->close_trigger_; break; default: @@ -115,13 +133,13 @@ void TimeBasedCover::start_direction_(CoverOperation dir) { this->current_operation = dir; - this->stop_prev_trigger_(); - trig->trigger(); - this->prev_command_trigger_ = trig; - const uint32_t now = millis(); this->start_dir_time_ = now; this->last_recompute_time_ = now; + + this->stop_prev_trigger_(); + trig->trigger(); + this->prev_command_trigger_ = trig; } void TimeBasedCover::recompute_position_() { if (this->current_operation == COVER_OPERATION_IDLE) diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 6c48c26ed1..517ab77cb3 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -45,6 +45,7 @@ class TimeBasedCover : public cover::Cover, public Component { float target_position_{0}; bool has_built_in_endstop_{false}; bool assumed_state_{false}; + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; } // namespace time_based diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py index f7cc89b252..ac45909673 100644 --- a/esphome/components/tlc59208f/output.py +++ b/esphome/components/tlc59208f/output.py @@ -20,6 +20,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): paren = await cg.get_variable(config[CONF_TLC59208F_ID]) - rhs = paren.create_channel(config[CONF_CHANNEL]) - var = cg.Pvariable(config[CONF_ID], rhs) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(paren.register_channel(var)) await output.register_output(var, config) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 784a0947ed..59fb9f98ed 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -75,7 +75,7 @@ void TLC59208FOutput::setup() { ESP_LOGV(TAG, " Resetting all devices on the bus..."); // Reset all devices on the bus - if (!this->parent_->write_byte(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ[0], TLC59208F_SWRST_SEQ[1])) { + if (this->bus_->write(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, 2) != i2c::ERROR_OK) { ESP_LOGE(TAG, "RESET failed"); this->mark_failed(); return; @@ -137,11 +137,11 @@ void TLC59208FOutput::loop() { this->update_ = false; } -TLC59208FChannel *TLC59208FOutput::create_channel(uint8_t channel) { - this->min_channel_ = std::min(this->min_channel_, channel); - this->max_channel_ = std::max(this->max_channel_, channel); - auto *c = new TLC59208FChannel(this, channel); - return c; +void TLC59208FOutput::register_channel(TLC59208FChannel *channel) { + auto c = channel->channel_; + this->min_channel_ = std::min(this->min_channel_, c); + this->max_channel_ = std::max(this->max_channel_, c); + channel->set_parent(this); } void TLC59208FChannel::write_state(float state) { diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 06b7adc882..68ca8061d7 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" @@ -21,14 +22,15 @@ extern const uint8_t TLC59208F_MODE2_WDT_35MS; class TLC59208FOutput; -class TLC59208FChannel : public output::FloatOutput { +class TLC59208FChannel : public output::FloatOutput, public Parented { public: - TLC59208FChannel(TLC59208FOutput *parent, uint8_t channel) : parent_(parent), channel_(channel) {} + void set_channel(uint8_t channel) { channel_ = channel; } protected: + friend class TLC59208FOutput; + void write_state(float state) override; - TLC59208FOutput *parent_; uint8_t channel_; }; @@ -37,7 +39,7 @@ class TLC59208FOutput : public Component, public i2c::I2CDevice { public: TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {} - TLC59208FChannel *create_channel(uint8_t channel); + void register_channel(TLC59208FChannel *channel); void setup() override; void dump_config() override; diff --git a/esphome/components/tlc5947/__init__.py b/esphome/components/tlc5947/__init__.py new file mode 100644 index 0000000000..84380bdace --- /dev/null +++ b/esphome/components/tlc5947/__init__.py @@ -0,0 +1,50 @@ +# this component is for the "TLC5947 24-Channel, 12-Bit PWM LED Driver" [https://www.ti.com/lit/ds/symlink/tlc5947.pdf], +# which is used e.g. on [https://www.adafruit.com/product/1429]. The code is based on the components sm2135 and sm26716. + +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, +) + +CONF_LAT_PIN = "lat_pin" +CONF_OE_PIN = "oe_pin" + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@rnauber"] + +tlc5947_ns = cg.esphome_ns.namespace("tlc5947") +TLC5947 = tlc5947_ns.class_("TLC5947", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC5947), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LAT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_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)) + lat = await cg.gpio_pin_expression(config[CONF_LAT_PIN]) + cg.add(var.set_lat_pin(lat)) + if CONF_OE_PIN in config: + outenable = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_outenable_pin(outenable)) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/tlc5947/output.py b/esphome/components/tlc5947/output.py new file mode 100644 index 0000000000..ece47fa63d --- /dev/null +++ b/esphome/components/tlc5947/output.py @@ -0,0 +1,28 @@ +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/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp new file mode 100644 index 0000000000..a7e08c8341 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -0,0 +1,65 @@ +#include "tlc5947.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tlc5947 { + +static const char *const TAG = "tlc5947"; + +void TLC5947::setup() { + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->lat_pin_->setup(); + this->lat_pin_->digital_write(true); + if (this->outenable_pin_ != nullptr) { + this->outenable_pin_->setup(); + this->outenable_pin_->digital_write(false); + } + + this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); + + ESP_LOGCONFIG(TAG, "Done setting up TLC5947 output component."); +} +void TLC5947::dump_config() { + ESP_LOGCONFIG(TAG, "TLC5947:"); + 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_); + ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); +} + +void TLC5947::loop() { + if (!this->update_) + return; + + this->lat_pin_->digital_write(false); + + // push the data out, MSB first, 12 bit word per channel, 24 channels per chip + for (int32_t ch = N_CHANNELS_PER_CHIP * num_chips_ - 1; ch >= 0; ch--) { + uint16_t word = pwm_amounts_[ch]; + for (uint8_t bit = 0; bit < 12; bit++) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(word & 0x800); + word <<= 1; + + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(true); // TWH0>12ns, so we should be fine using this as delay + } + } + + this->clock_pin_->digital_write(false); + + // latch the values, so they will be applied + this->lat_pin_->digital_write(true); + delayMicroseconds(1); // TWH1 > 30ns + this->lat_pin_->digital_write(false); + + this->update_ = false; +} + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tlc5947/tlc5947.h b/esphome/components/tlc5947/tlc5947.h new file mode 100644 index 0000000000..0eb7f10604 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.h @@ -0,0 +1,70 @@ +#pragma once +// TLC5947 24-Channel, 12-Bit PWM LED Driver +// https://www.ti.com/lit/ds/symlink/tlc5947.pdf + +#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; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_lat_pin(GPIOPin *lat_pin) { lat_pin_ = lat_pin; } + void set_outenable_pin(GPIOPin *outenable_pin) { outenable_pin_ = outenable_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; + + 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_; + }; + + 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_; + GPIOPin *outenable_pin_{nullptr}; + uint8_t num_chips_; + + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 1f1c0fd301..488f3b6727 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -1,6 +1,7 @@ #include "tm1637.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace tm1637 { @@ -146,16 +147,16 @@ void TM1637Display::update() { float TM1637Display::get_setup_priority() const { return setup_priority::PROCESSOR; } void TM1637Display::bit_delay_() { delayMicroseconds(100); } void TM1637Display::start_() { - this->dio_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); } void TM1637Display::stop_() { - this->dio_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); bit_delay_(); - this->clk_pin_->pin_mode(INPUT); + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); bit_delay_(); - this->dio_pin_->pin_mode(INPUT); + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); bit_delay_(); } @@ -189,39 +190,39 @@ bool TM1637Display::send_byte_(uint8_t b) { // 8 Data Bits for (uint8_t i = 0; i < 8; i++) { // CLK low - this->clk_pin_->pin_mode(OUTPUT); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); // Set data bit if (data & 0x01) - this->dio_pin_->pin_mode(INPUT); + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); else - this->dio_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); // CLK high - this->clk_pin_->pin_mode(INPUT); + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); data = data >> 1; } // Wait for acknowledge // CLK to zero - this->clk_pin_->pin_mode(OUTPUT); - this->dio_pin_->pin_mode(INPUT); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); // CLK to high - this->clk_pin_->pin_mode(INPUT); + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); uint8_t ack = this->dio_pin_->digital_read(); if (ack == 0) { - this->dio_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); } this->bit_delay_(); - this->clk_pin_->pin_mode(OUTPUT); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); return ack; @@ -233,7 +234,7 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') - data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); + data = progmem_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); if (data == TM1637_UNKNOWN_CHAR) { ESP_LOGW(TAG, "Encountered character '%c' with no TM1637 representation while translating string!", *str); diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 003344eae9..63b30ac13e 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index f67a9f4512..9d2b17afdc 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -27,12 +27,15 @@ TM1651_BRIGHTNESS_OPTIONS = { 3: TM1651Display.TM1651_BRIGHTNESS_HIGH, } -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(TM1651Display), - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, - } +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TM1651Display), + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, + } + ), + cv.only_with_arduino, ) validate_level_percent = cv.All(cv.int_range(min=0, max=100)) @@ -50,7 +53,7 @@ async def to_code(config): cg.add(var.set_dio_pin(dio_pin)) # https://platformio.org/lib/show/6865/TM1651 - cg.add_library("6865", "1.0.1") + cg.add_library("freekode/TM1651", "1.0.1") BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index bb37e9f4c9..c6bb1bc025 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -1,5 +1,8 @@ +#ifdef USE_ARDUINO + #include "tm1651.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace tm1651 { @@ -19,7 +22,7 @@ void TM1651Display::setup() { uint8_t clk = clk_pin_->get_pin(); uint8_t dio = dio_pin_->get_pin(); - battery_display_ = new TM1651(clk, dio); + battery_display_ = make_unique(clk, dio); battery_display_->init(); battery_display_->clearDisplay(); } @@ -87,3 +90,5 @@ uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { } // namespace tm1651 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 6eab24687c..72849bc8eb 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -1,7 +1,11 @@ #pragma once +#ifdef USE_ARDUINO + +#include + #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/automation.h" #include @@ -11,8 +15,8 @@ namespace tm1651 { class TM1651Display : public Component { public: - void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } - void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + void set_clk_pin(InternalGPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(InternalGPIOPin *pin) { dio_pin_ = pin; } void setup() override; void dump_config() override; @@ -25,9 +29,9 @@ class TM1651Display : public Component { void turn_off(); protected: - TM1651 *battery_display_; - GPIOPin *clk_pin_; - GPIOPin *dio_pin_; + std::unique_ptr battery_display_; + InternalGPIOPin *clk_pin_; + InternalGPIOPin *dio_pin_; bool is_on_ = true; uint8_t brightness_; @@ -81,3 +85,5 @@ template class TurnOffAction : public Action, public Pare } // namespace tm1651 } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index b54d5646ba..c5ffbb8df5 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -13,7 +13,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -28,7 +27,10 @@ TMP102Component = tmp102_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 7b3dcad4aa..f6bb9a05c0 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -1,5 +1,6 @@ #include "tmp102.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace tmp102 { @@ -28,10 +29,16 @@ void TMP102Component::dump_config() { void TMP102Component::update() { uint16_t raw_temperature; - if (!this->read_byte_16(TMP102_REGISTER_TEMPERATURE, &raw_temperature, 50)) { + 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); raw_temperature = raw_temperature >> 4; float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index a5fc027b20..054864dd83 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -5,12 +5,12 @@ from esphome.const import ( CONF_ID, CONF_UPDATE_INTERVAL, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] tmp117_ns = cg.esphome_ns.namespace("tmp117") TMP117Component = tmp117_ns.class_( @@ -19,7 +19,10 @@ TMP117Component = tmp117_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py index 2110cbfcf8..2d3add2399 100644 --- a/esphome/components/tof10120/sensor.py +++ b/esphome/components/tof10120/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -19,11 +18,10 @@ TOF10120Sensor = tof10120_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp index cabfdad41d..4ba591f9c4 100644 --- a/esphome/components/tof10120/tof10120_sensor.cpp +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -1,5 +1,6 @@ #include "tof10120_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" // Very basic support for TOF10120 distance sensor @@ -31,7 +32,12 @@ void TOF10120Sensor::update() { } uint8_t data[2]; - if (!this->read_bytes(TOF10120_DISTANCE_REGISTER, data, 2, TOF10120_DEFAULT_DELAY)) { + if (this->write(&TOF10120_DISTANCE_REGISTER, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + delay(TOF10120_DEFAULT_DELAY); + if (this->read(data, 2) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with TOF10120 failed on read"); this->status_set_warning(); return; diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 95c9f1f127..3f2c644c87 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,16 +1,25 @@ 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_MODEL AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@kbx81"] toshiba_ns = cg.esphome_ns.namespace("toshiba") ToshibaClimate = toshiba_ns.class_("ToshibaClimate", climate_ir.ClimateIR) +Model = toshiba_ns.enum("Model") +MODELS = { + "GENERIC": Model.MODEL_GENERIC, + "RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C, + "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, +} + CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ToshibaClimate), + cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } ) @@ -18,3 +27,4 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_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_model(config[CONF_MODEL])) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 4f5b7d8537..25528abbe1 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -3,13 +3,23 @@ namespace esphome { namespace toshiba { +struct RacPt1411hwruFanSpeed { + uint8_t code1; + uint8_t code2; +}; + +static const char *const TAG = "toshiba.climate"; +// Timings for IR bits/data const uint16_t TOSHIBA_HEADER_MARK = 4380; const uint16_t TOSHIBA_HEADER_SPACE = 4370; const uint16_t TOSHIBA_GAP_SPACE = 5480; +const uint16_t TOSHIBA_PACKET_SPACE = 10500; const uint16_t TOSHIBA_BIT_MARK = 540; const uint16_t TOSHIBA_ZERO_SPACE = 540; const uint16_t TOSHIBA_ONE_SPACE = 1620; - +const uint16_t TOSHIBA_CARRIER_FREQUENCY = 38000; +const uint8_t TOSHIBA_HEADER_LENGTH = 4; +// Generic Toshiba commands/flags const uint8_t TOSHIBA_COMMAND_DEFAULT = 0x01; const uint8_t TOSHIBA_COMMAND_TIMER = 0x02; const uint8_t TOSHIBA_COMMAND_POWER = 0x08; @@ -36,36 +46,122 @@ const uint8_t TOSHIBA_POWER_ECO = 0x03; const uint8_t TOSHIBA_MOTION_SWING = 0x04; const uint8_t TOSHIBA_MOTION_FIX = 0x00; -static const char *const TAG = "toshiba.climate"; +// RAC-PT1411HWRU temperature code flag bits +const uint8_t RAC_PT1411HWRU_FLAG_FAH = 0x01; +const uint8_t RAC_PT1411HWRU_FLAG_FRAC = 0x20; +const uint8_t RAC_PT1411HWRU_FLAG_NEG = 0x10; +// RAC-PT1411HWRU temperature short code flags mask +const uint8_t RAC_PT1411HWRU_FLAG_MASK = 0x0F; +// RAC-PT1411HWRU Headers, Footers and such +const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER0 = 0xB2; +const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER1 = 0xD5; +const uint8_t RAC_PT1411HWRU_MESSAGE_LENGTH = 6; +// RAC-PT1411HWRU "Comfort Sense" feature bits +const uint8_t RAC_PT1411HWRU_CS_ENABLED = 0x40; +const uint8_t RAC_PT1411HWRU_CS_DATA = 0x80; +const uint8_t RAC_PT1411HWRU_CS_HEADER = 0xBA; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_AUTO = 0x7A; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL = 0x72; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT = 0x7E; +// RAC-PT1411HWRU Swing +const uint8_t RAC_PT1411HWRU_SWING_HEADER = 0xB9; +const std::vector RAC_PT1411HWRU_SWING_VERTICAL{0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB}; +const std::vector RAC_PT1411HWRU_SWING_OFF{0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA}; +// RAC-PT1411HWRU Fan speeds +const uint8_t RAC_PT1411HWRU_FAN_OFF = 0x7B; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO{0xBF, 0x66}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_LOW{0x9F, 0x28}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_MED{0x5F, 0x3C}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_HIGH{0x3F, 0x64}; +// RAC-PT1411HWRU Fan speed for Auto and Dry climate modes +const RacPt1411hwruFanSpeed RAC_PT1411HWRU_NO_FAN{0x1F, 0x65}; +// RAC-PT1411HWRU Modes +const uint8_t RAC_PT1411HWRU_MODE_AUTO = 0x08; +const uint8_t RAC_PT1411HWRU_MODE_COOL = 0x00; +const uint8_t RAC_PT1411HWRU_MODE_DRY = 0x04; +const uint8_t RAC_PT1411HWRU_MODE_FAN = 0x04; +const uint8_t RAC_PT1411HWRU_MODE_HEAT = 0x0C; +const uint8_t RAC_PT1411HWRU_MODE_OFF = 0x00; +// RAC-PT1411HWRU Fan-only "temperature"/system off +const uint8_t RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY = 0x0E; +// RAC-PT1411HWRU temperature codes are not sequential; they instead follow a modified Gray code. +// Hence these look-up tables. In addition, the upper nibble is used here for additional +// "negative" and "fractional value" flags as required for some temperatures. +// RAC-PT1411HWRU °C Temperatures (short codes) +const std::vector RAC_PT1411HWRU_TEMPERATURE_C{0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05, + 0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B}; +// RAC-PT1411HWRU °F Temperatures (short codes) +const std::vector RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02, + 0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C, + 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B}; + +void ToshibaClimate::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + this->transmit_rac_pt1411hwru_temp_(); + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = + roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + // Set supported modes & temperatures based on model + this->minimum_temperature_ = this->temperature_min_(); + this->maximum_temperature_ = this->temperature_max_(); + this->supports_dry_ = this->toshiba_supports_dry_(); + this->supports_fan_only_ = this->toshiba_supports_fan_only_(); + this->fan_modes_ = this->toshiba_fan_modes_(); + this->swing_modes_ = this->toshiba_swing_modes_(); + // Never send nan to HA + if (std::isnan(this->target_temperature)) + this->target_temperature = 24; +} void ToshibaClimate::transmit_state() { + if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) { + transmit_rac_pt1411hwru_(); + } else { + transmit_generic_(); + } +} + +void ToshibaClimate::transmit_generic_() { uint8_t message[16] = {0}; uint8_t message_length = 9; - /* Header */ + // Header message[0] = 0xf2; message[1] = 0x0d; - /* Message length */ + // Message length message[2] = message_length - 6; - /* First checksum */ + // First checksum message[3] = message[0] ^ message[1] ^ message[2]; - /* Command */ + // Command message[4] = TOSHIBA_COMMAND_DEFAULT; - /* Temperature */ - uint8_t temperature = static_cast(this->target_temperature); - if (temperature < 17) { - temperature = 17; - } - if (temperature > 30) { - temperature = 30; - } - message[5] = (temperature - 17) << 4; + // Temperature + uint8_t temperature = static_cast( + clamp(this->target_temperature, TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX)); + message[5] = (temperature - static_cast(TOSHIBA_GENERIC_TEMP_C_MIN)) << 4; - /* Mode and fan */ + // Mode and fan uint8_t mode; switch (this->mode) { case climate::CLIMATE_MODE_OFF: @@ -80,35 +176,511 @@ void ToshibaClimate::transmit_state() { mode = TOSHIBA_MODE_COOL; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: default: mode = TOSHIBA_MODE_AUTO; } message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; - /* Zero */ + // Zero message[7] = 0x00; - /* If timers bit in the command is set, two extra bytes are added here */ + // If timers bit in the command is set, two extra bytes are added here - /* If power bit is set in the command, one extra byte is added here */ + // If power bit is set in the command, one extra byte is added here - /* The last byte is the xor of all bytes from [4] */ + // The last byte is the xor of all bytes from [4] for (uint8_t i = 4; i < 8; i++) { message[8] ^= message[i]; } - /* Transmit */ + // Transmit auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); - data->set_carrier_frequency(38000); - for (uint8_t copy = 0; copy < 2; copy++) { - data->mark(TOSHIBA_HEADER_MARK); - data->space(TOSHIBA_HEADER_SPACE); + encode_(data, message, message_length, 1); - for (uint8_t byte = 0; byte < message_length; byte++) { + transmit.perform(); +} + +void ToshibaClimate::transmit_rac_pt1411hwru_() { + uint8_t code = 0, index = 0, message[RAC_PT1411HWRU_MESSAGE_LENGTH * 2] = {0}; + float temperature = + clamp(this->target_temperature, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX); + float temp_adjd = temperature - TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + // Byte 0: Header upper (0xB2) + message[0] = RAC_PT1411HWRU_MESSAGE_HEADER0; + // Byte 1: Header lower (0x4D) + message[1] = ~message[0]; + // Byte 2u: Fan speed + // Byte 2l: 1111 (on) or 1011 (off) + if (this->mode == climate::CLIMATE_MODE_OFF) { + message[2] = RAC_PT1411HWRU_FAN_OFF; + } else if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_DRY)) { + message[2] = RAC_PT1411HWRU_NO_FAN.code1; + message[7] = RAC_PT1411HWRU_NO_FAN.code2; + } else { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + message[2] = RAC_PT1411HWRU_FAN_LOW.code1; + message[7] = RAC_PT1411HWRU_FAN_LOW.code2; + break; + + case climate::CLIMATE_FAN_MEDIUM: + message[2] = RAC_PT1411HWRU_FAN_MED.code1; + message[7] = RAC_PT1411HWRU_FAN_MED.code2; + break; + + case climate::CLIMATE_FAN_HIGH: + message[2] = RAC_PT1411HWRU_FAN_HIGH.code1; + message[7] = RAC_PT1411HWRU_FAN_HIGH.code2; + break; + + case climate::CLIMATE_FAN_AUTO: + default: + message[2] = RAC_PT1411HWRU_FAN_AUTO.code1; + message[7] = RAC_PT1411HWRU_FAN_AUTO.code2; + } + } + // Byte 3u: ~Fan speed + // Byte 3l: 0000 (on) or 0100 (off) + message[3] = ~message[2]; + // Byte 4u: Temp + if (this->model_ == MODEL_RAC_PT1411HWRU_F) { + temperature = (temperature * 1.8) + 32; + temp_adjd = temperature - TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN; + } + + index = static_cast(roundf(temp_adjd)); + + if (this->model_ == MODEL_RAC_PT1411HWRU_F) { + code = RAC_PT1411HWRU_TEMPERATURE_F[index]; + message[9] |= RAC_PT1411HWRU_FLAG_FAH; + } else { + code = RAC_PT1411HWRU_TEMPERATURE_C[index]; + } + if ((this->mode == climate::CLIMATE_MODE_FAN_ONLY) || (this->mode == climate::CLIMATE_MODE_OFF)) { + code = RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY; + } + + if (code & RAC_PT1411HWRU_FLAG_FRAC) { + message[8] |= RAC_PT1411HWRU_FLAG_FRAC; + } + if (code & RAC_PT1411HWRU_FLAG_NEG) { + message[9] |= RAC_PT1411HWRU_FLAG_NEG; + } + message[4] = (code & RAC_PT1411HWRU_FLAG_MASK) << 4; + // Byte 4l: Mode + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + // zerooooo + break; + + case climate::CLIMATE_MODE_HEAT: + message[4] |= RAC_PT1411HWRU_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + message[4] |= RAC_PT1411HWRU_MODE_COOL; + break; + + case climate::CLIMATE_MODE_DRY: + message[4] |= RAC_PT1411HWRU_MODE_DRY; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + message[4] |= RAC_PT1411HWRU_MODE_FAN; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + default: + message[4] |= RAC_PT1411HWRU_MODE_AUTO; + } + + // Byte 5u: ~Temp + // Byte 5l: ~Mode + message[5] = ~message[4]; + + if (this->mode != climate::CLIMATE_MODE_OFF) { + // Byte 6: Header (0xD5) + message[6] = RAC_PT1411HWRU_MESSAGE_HEADER1; + // Byte 7: Fan speed part 2 (done above) + // Byte 8: 0x20 for °F frac, else 0 (done above) + // Byte 9: 0x10=NEG, 0x01=°F (done above) + // Byte 10: 0 + // Byte 11: Checksum (bytes 6 through 10) + for (index = 6; index <= 10; index++) { + message[11] += message[index]; + } + } + ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], + message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10], + message[11]); + + // load first block of IR code and repeat it once + encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + // load second block of IR code, if present + if (message[6] != 0) { + encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0); + } + + transmit.perform(); + + // Swing Mode + data->reset(); + data->space(TOSHIBA_PACKET_SPACE); + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + break; + + case climate::CLIMATE_SWING_OFF: + default: + encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + } + + data->space(TOSHIBA_PACKET_SPACE); + transmit.perform(); + + if (this->sensor_) { + transmit_rac_pt1411hwru_temp_(true, false); + } +} + +void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bool cs_send_update) { + if ((this->mode == climate::CLIMATE_MODE_HEAT) || (this->mode == climate::CLIMATE_MODE_COOL) || + (this->mode == climate::CLIMATE_MODE_HEAT_COOL)) { + uint8_t message[RAC_PT1411HWRU_MESSAGE_LENGTH] = {0}; + float temperature = clamp(this->current_temperature, 0.0, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX + 1); + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + // "Comfort Sense" feature notes + // IR Code: 0xBA45 xxXX yyYY + // xx: Temperature in °C + // Bit 6: feature state (on/off) + // Bit 7: message contains temperature data for feature (bit 6 must also be set) + // XX: Bitwise complement of xx + // yy: Mode: Auto=0x7A, Cool=0x72, Heat=0x7E + // YY: Bitwise complement of yy + // + // Byte 0: Header upper (0xBA) + message[0] = RAC_PT1411HWRU_CS_HEADER; + // Byte 1: Header lower (0x45) + message[1] = ~message[0]; + // Byte 2: Temperature in °C + message[2] = static_cast(roundf(temperature)); + if (cs_send_update) { + message[2] |= RAC_PT1411HWRU_CS_ENABLED | RAC_PT1411HWRU_CS_DATA; + } else if (cs_state) { + message[2] |= RAC_PT1411HWRU_CS_ENABLED; + } + // Byte 3: Bitwise complement of byte 2 + message[3] = ~message[2]; + // Byte 4: Footer upper + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + message[4] = RAC_PT1411HWRU_CS_FOOTER_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + message[4] = RAC_PT1411HWRU_CS_FOOTER_COOL; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + message[4] = RAC_PT1411HWRU_CS_FOOTER_AUTO; + + default: + break; + } + // Byte 5: Footer lower/bitwise complement of byte 4 + message[5] = ~message[4]; + + ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3], + message[4], message[5]); + // load IR code and repeat it once + encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + + transmit.perform(); + } +} + +uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { + const std::vector header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, + RAC_PT1411HWRU_SWING_HEADER}; + + for (auto i : header) { + if ((message[0] == i) && (message[1] == static_cast(~i))) + return i; + } + if (message[0] == RAC_PT1411HWRU_MESSAGE_HEADER1) + return RAC_PT1411HWRU_MESSAGE_HEADER1; + + return 0; +} + +bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2) { + for (uint8_t i = 0; i < RAC_PT1411HWRU_MESSAGE_LENGTH; i++) { + if (message1[i] != message2[i]) + return false; + } + return true; +} + +bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) { + uint8_t checksum = 0; + + switch (is_valid_rac_pt1411hwru_header_(message)) { + case RAC_PT1411HWRU_MESSAGE_HEADER0: + case RAC_PT1411HWRU_CS_HEADER: + case RAC_PT1411HWRU_SWING_HEADER: + if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast(~message[3])) && + (message[4] == static_cast(~message[5]))) { + return true; + } + break; + + case RAC_PT1411HWRU_MESSAGE_HEADER1: + for (uint8_t i = 0; i < RAC_PT1411HWRU_MESSAGE_LENGTH - 1; i++) { + checksum += message[i]; + } + if (checksum == message[RAC_PT1411HWRU_MESSAGE_LENGTH - 1]) { + return true; + } + break; + + default: + return false; + } + + return false; +} + +bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t message[18] = {0}; + uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0; + + // Validate header + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + // Read incoming bits into buffer + if (!decode_(&data, message, message_length)) { + return false; + } + // Determine incoming message protocol version and/or length + if (is_valid_rac_pt1411hwru_header_(message)) { + // We already received four bytes + message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4; + } else if ((message[0] ^ message[1] ^ message[2]) != message[3]) { + // Return false if first checksum was not valid + return false; + } else { + // First checksum was valid so continue receiving the remaining bits + message_length = message[2] + 2; + } + // Decode the remaining bytes + if (!decode_(&data, &message[4], message_length)) { + return false; + } + // If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet + if (is_valid_rac_pt1411hwru_header_(message)) { + // There is always a space between packets + if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { + return false; + } + // Validate header 2 + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + return false; + } + // If this is a RAC-PT1411HWRU message, there may also be a third packet. + // We do not fail the receive if we don't get this; it isn't always present + if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { + // Validate header 3 + data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE); + if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + if (!is_valid_rac_pt1411hwru_message_(&message[12])) { + // If a third packet was received but the checksum is not valid, fail + return false; + } + } + } + if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) { + // If the first two packets don't match each other, fail + return false; + } + if (!is_valid_rac_pt1411hwru_message_(&message[0])) { + // If the first packet isn't valid, fail + return false; + } + } + + // Header has been verified, now determine protocol version and set the climate component properties + switch (is_valid_rac_pt1411hwru_header_(message)) { + // Power, temperature, mode, fan speed + case RAC_PT1411HWRU_MESSAGE_HEADER0: + // Get the mode + switch (message[4] & 0x0F) { + case RAC_PT1411HWRU_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + + // case RAC_PT1411HWRU_MODE_OFF: + case RAC_PT1411HWRU_MODE_COOL: + if (((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) && (message[2] == RAC_PT1411HWRU_FAN_OFF)) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + break; + + // case RAC_PT1411HWRU_MODE_DRY: + case RAC_PT1411HWRU_MODE_FAN: + if ((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + else + this->mode = climate::CLIMATE_MODE_DRY; + break; + + case RAC_PT1411HWRU_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + default: + this->mode = climate::CLIMATE_MODE_OFF; + break; + } + // Get the fan speed/mode + switch (message[2]) { + case RAC_PT1411HWRU_FAN_LOW.code1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + + case RAC_PT1411HWRU_FAN_MED.code1: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + + case RAC_PT1411HWRU_FAN_HIGH.code1: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + + case RAC_PT1411HWRU_FAN_AUTO.code1: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + // Get the target temperature + if (is_valid_rac_pt1411hwru_message_(&message[12])) { + temperature_code = + (message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG); + if (message[15] & RAC_PT1411HWRU_FLAG_FAH) { + for (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_F.size(); i++) { + if (RAC_PT1411HWRU_TEMPERATURE_F[i] == temperature_code) { + this->target_temperature = static_cast((i + TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN - 32) * 5) / 9; + } + } + } else { + for (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_C.size(); i++) { + if (RAC_PT1411HWRU_TEMPERATURE_C[i] == temperature_code) { + this->target_temperature = i + TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + } + } + } + } + break; + // "Comfort Sense" temperature packet + case RAC_PT1411HWRU_CS_HEADER: + // "Comfort Sense" feature notes + // IR Code: 0xBA45 xxXX yyYY + // xx: Temperature in °C + // Bit 6: feature state (on/off) + // Bit 7: message contains temperature data for feature (bit 6 must also be set) + // XX: Bitwise complement of xx + // yy: Mode: Auto: 7A + // Cool: 72 + // Heat: 7E + // YY: Bitwise complement of yy + if ((message[2] & RAC_PT1411HWRU_CS_ENABLED) && (message[2] & RAC_PT1411HWRU_CS_DATA)) { + // Setting current_temperature this way allows the unit's remote to provide the temperature to HA + this->current_temperature = message[2] & ~(RAC_PT1411HWRU_CS_ENABLED | RAC_PT1411HWRU_CS_DATA); + } + break; + // Swing mode + case RAC_PT1411HWRU_SWING_HEADER: + if (message[4] == RAC_PT1411HWRU_SWING_VERTICAL[4]) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + break; + // Generic (old) Toshiba packet + default: + uint8_t checksum = 0; + // Add back the length of the header (we pruned it above) + message_length += TOSHIBA_HEADER_LENGTH; + // Validate the second checksum before trusting any more of the message + for (uint8_t i = TOSHIBA_HEADER_LENGTH; i < message_length - 1; i++) { + checksum ^= message[i]; + } + // Did our computed checksum and the provided checksum match? + if (checksum != message[message_length - 1]) { + return false; + } + // Check if this is a short swing/fix message + if (message[4] & TOSHIBA_COMMAND_MOTION) { + // Not supported yet + return false; + } + + // Get the mode + switch (message[6] & 0x0F) { + case TOSHIBA_MODE_OFF: + this->mode = climate::CLIMATE_MODE_OFF; + break; + + case TOSHIBA_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + + case TOSHIBA_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + + case TOSHIBA_MODE_FAN_ONLY: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + + case TOSHIBA_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + case TOSHIBA_MODE_AUTO: + default: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + } + + // Get the target temperature + this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN; + } + + this->publish_state(); + return true; +} + +void ToshibaClimate::encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, const uint8_t nbytes, + const uint8_t repeat) { + data->set_carrier_frequency(TOSHIBA_CARRIER_FREQUENCY); + + for (uint8_t copy = 0; copy <= repeat; copy++) { + data->item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE); + + for (uint8_t byte = 0; byte < nbytes; byte++) { for (uint8_t bit = 0; bit < 8; bit++) { data->mark(TOSHIBA_BIT_MARK); if (message[byte] & (1 << (7 - bit))) { @@ -118,87 +690,24 @@ void ToshibaClimate::transmit_state() { } } } - - data->mark(TOSHIBA_BIT_MARK); - data->space(TOSHIBA_GAP_SPACE); + data->item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE); } - - transmit.perform(); } -bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t message[16] = {0}; - uint8_t message_length = 4; - - /* Validate header */ - if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { - return false; - } - - /* Decode bytes */ - for (uint8_t byte = 0; byte < message_length; byte++) { +bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *message, const uint8_t nbytes) { + for (uint8_t byte = 0; byte < nbytes; byte++) { for (uint8_t bit = 0; bit < 8; bit++) { - if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { + if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { message[byte] |= 1 << (7 - bit); - } else if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { - /* Bit is already clear */ + } else if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + message[byte] &= static_cast(~(1 << (7 - bit))); } else { return false; } } - - /* Update length */ - if (byte == 3) { - /* Validate the first checksum before trusting the length field */ - if ((message[0] ^ message[1] ^ message[2]) != message[3]) { - return false; - } - message_length = message[2] + 6; - } } - - /* Validate the second checksum before trusting any more of the message */ - uint8_t checksum = 0; - for (uint8_t i = 4; i < message_length - 1; i++) { - checksum ^= message[i]; - } - - if (checksum != message[message_length - 1]) { - return false; - } - - /* Check if this is a short swing/fix message */ - if (message[4] & TOSHIBA_COMMAND_MOTION) { - /* Not supported yet */ - return false; - } - - /* Get the mode. */ - switch (message[6] & 0x0f) { - case TOSHIBA_MODE_OFF: - this->mode = climate::CLIMATE_MODE_OFF; - break; - - case TOSHIBA_MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - break; - - case TOSHIBA_MODE_COOL: - this->mode = climate::CLIMATE_MODE_COOL; - break; - - case TOSHIBA_MODE_AUTO: - default: - /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ - this->mode = climate::CLIMATE_MODE_AUTO; - } - - /* Get the target temperature */ - this->target_temperature = (message[5] >> 4) + 17; - - this->publish_state(); return true; } -} /* namespace toshiba */ -} /* namespace esphome */ +} // namespace toshiba +} // namespace esphome diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 3ab0dcdcdb..36e8760169 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -5,17 +5,69 @@ namespace esphome { namespace toshiba { -const float TOSHIBA_TEMP_MIN = 17.0; -const float TOSHIBA_TEMP_MAX = 30.0; +// Simple enum to represent models. +enum Model { + MODEL_GENERIC = 0, // Temperature range is from 17 to 30 + MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30 + MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30 +}; + +// Supported temperature ranges +const float TOSHIBA_GENERIC_TEMP_C_MIN = 17.0; +const float TOSHIBA_GENERIC_TEMP_C_MAX = 30.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0; class ToshibaClimate : public climate_ir::ClimateIR { public: - ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX, 1.0f) {} + ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f) {} + + void setup() override; + void set_model(Model model) { this->model_ = model; } protected: void transmit_state() override; + void transmit_generic_(); + void transmit_rac_pt1411hwru_(); + void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true); + // Returns the header if valid, else returns zero + uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message); + // Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet + bool is_valid_rac_pt1411hwru_message_(const uint8_t *message); + // Returns true if message1 and message 2 are the same + bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2); bool on_receive(remote_base::RemoteReceiveData data) override; + + float temperature_min_() { + return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + } + float temperature_max_() { + return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; + } + bool toshiba_supports_dry_() { + return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); + } + bool toshiba_supports_fan_only_() { + return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); + } + std::set toshiba_fan_modes_() { + return (this->model_ == MODEL_GENERIC) + ? std::set{} + : std::set{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}; + } + std::set toshiba_swing_modes_() { + return (this->model_ == MODEL_GENERIC) + ? std::set{} + : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + } + void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); + bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); + + Model model_; }; -} /* namespace toshiba */ -} /* namespace esphome */ +} // namespace toshiba +} // namespace esphome diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index b70a0cece9..6a8a416b81 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -1,23 +1,63 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, time -from esphome.const import CONF_ID, CONF_TIME_ID +from esphome.const import ( + CONF_ICON, + CONF_ID, + CONF_TIME_ID, + DEVICE_CLASS_ENERGY, + CONF_METHOD, + STATE_CLASS_TOTAL_INCREASING, +) +from esphome.core.entity_helpers import inherit_property_from DEPENDENCIES = ["time"] CONF_POWER_ID = "power_id" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") +TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod") +TOTAL_DAILY_ENERGY_METHODS = { + "trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID, + "left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT, + "right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT, +} TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TotalDailyEnergy), - cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TotalDailyEnergy), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + cv.Optional( + CONF_MIN_SAVE_INTERVAL, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_METHOD, default="right"): cv.enum( + TOTAL_DAILY_ENERGY_METHODS, lower=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(TotalDailyEnergy), + cv.Optional(CONF_ICON): cv.icon, + cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + }, + extra=cv.ALLOW_EXTRA, + ), + inherit_property_from(CONF_ICON, CONF_POWER_ID), +) async def to_code(config): @@ -30,3 +70,5 @@ async def to_code(config): cg.add(var.set_parent(sens)) time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) + cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) + cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 8c5ef8c137..178dc7cbe0 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -7,7 +7,7 @@ namespace total_daily_energy { static const char *const TAG = "total_daily_energy"; void TotalDailyEnergy::setup() { - this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); float recovered; if (this->pref_.load(&recovered)) { @@ -16,10 +16,13 @@ void TotalDailyEnergy::setup() { this->publish_state_and_save(0); } this->last_update_ = millis(); + this->last_save_ = this->last_update_; this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); } + void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } + void TotalDailyEnergy::loop() { auto t = this->time_->now(); if (!t.is_valid()) @@ -36,18 +39,40 @@ void TotalDailyEnergy::loop() { this->publish_state_and_save(0); } } + void TotalDailyEnergy::publish_state_and_save(float state) { - this->pref_.save(&state); this->total_energy_ = state; this->publish_state(state); + const uint32_t now = millis(); + if (now - this->last_save_ < this->min_save_interval_) { + return; + } + this->last_save_ = now; + this->pref_.save(&state); } + void TotalDailyEnergy::process_new_state_(float state) { - if (isnan(state)) + if (std::isnan(state)) return; const uint32_t now = millis(); + const float old_state = this->last_power_state_; + const float new_state = state; float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_energy = 0.0f; + switch (this->method_) { + case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: + delta_energy = delta_hours * (old_state + new_state) / 2.0; + break; + case TOTAL_DAILY_ENERGY_METHOD_LEFT: + delta_energy = delta_hours * old_state; + break; + case TOTAL_DAILY_ENERGY_METHOD_RIGHT: + delta_energy = delta_hours * new_state; + break; + } + this->last_power_state_ = new_state; this->last_update_ = now; - this->publish_state_and_save(this->total_energy_ + state * delta_hours); + this->publish_state_and_save(this->total_energy_ + delta_energy); } } // namespace total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index ae44125ffb..fedceafbd3 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -2,21 +2,29 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/time/real_time_clock.h" namespace esphome { namespace total_daily_energy { +enum TotalDailyEnergyMethod { + TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, + TOTAL_DAILY_ENERGY_METHOD_LEFT, + TOTAL_DAILY_ENERGY_METHOD_RIGHT, +}; + class TotalDailyEnergy : public sensor::Sensor, public Component { public: + void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_time(time::RealTimeClock *time) { time_ = time; } void set_parent(Sensor *parent) { parent_ = parent; } + void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } std::string unit_of_measurement() override { return this->parent_->get_unit_of_measurement() + "h"; } - std::string icon() override { return this->parent_->get_icon(); } int8_t accuracy_decimals() override { return this->parent_->get_accuracy_decimals() + 2; } void loop() override; @@ -28,9 +36,13 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { ESPPreferenceObject pref_; time::RealTimeClock *time_; Sensor *parent_; + TotalDailyEnergyMethod method_; uint16_t last_day_of_year_{}; uint32_t last_update_{0}; + uint32_t last_save_{0}; + uint32_t min_save_interval_{0}; float total_energy_{0.0f}; + float last_power_state_{0.0f}; }; } // namespace total_daily_energy diff --git a/esphome/components/tsl2561/sensor.py b/esphome/components/tsl2561/sensor.py index c05079f668..cf3837cb4d 100644 --- a/esphome/components/tsl2561/sensor.py +++ b/esphome/components/tsl2561/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, ) @@ -41,7 +40,10 @@ TSL2561Sensor = tsl2561_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tsl2591/__init__.py b/esphome/components/tsl2591/__init__.py new file mode 100644 index 0000000000..63331641c5 --- /dev/null +++ b/esphome/components/tsl2591/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@wjcarpenter"] diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py new file mode 100644 index 0000000000..095a8c886c --- /dev/null +++ b/esphome/components/tsl2591/sensor.py @@ -0,0 +1,168 @@ +# Credit where due.... +# I put a certain amount of work into this, but a lot of ESPHome integration is +# "look for other examples and see what they do" programming-by-example. Here are +# things that helped me along with this: +# +# - I mined the existing tsl2561 integration for basic structural framing for both +# the code and documentation. +# +# - I looked at the existing bme280 integration as an example of a single device +# with multiple sensors. +# +# - Comments and code in this thread got me going with the Adafruit TSL2591 library +# and prompted my desired to have tsl2591 as a standard component instead of a +# custom/external component. +# +# - And, of course, the handy and available Adafruit TSL2591 library was very +# helpful in understanding what the device is actually talking about. +# +# Here is the project that started me down the TSL2591 device trail in the first +# place: https://hackaday.io/project/176690-the-water-watcher + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_NAME, + CONF_INTEGRATION_TIME, + CONF_FULL_SPECTRUM, + CONF_INFRARED, + CONF_POWER_SAVE_MODE, + CONF_VISIBLE, + CONF_CALCULATED_LUX, + CONF_DEVICE_FACTOR, + CONF_GLASS_ATTENUATION_FACTOR, + ICON_BRIGHTNESS_6, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_EMPTY, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] + +tsl2591_ns = cg.esphome_ns.namespace("tsl2591") + +TSL2591IntegrationTime = tsl2591_ns.enum("TSL2591IntegrationTime") +INTEGRATION_TIMES = { + 100: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_100MS, + 200: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_200MS, + 300: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_300MS, + 400: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_400MS, + 500: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_500MS, + 600: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_600MS, +} + +TSL2591Gain = tsl2591_ns.enum("TSL2591Gain") +GAINS = { + "1X": TSL2591Gain.TSL2591_GAIN_LOW, + "LOW": TSL2591Gain.TSL2591_GAIN_LOW, + "25X": TSL2591Gain.TSL2591_GAIN_MED, + "MED": TSL2591Gain.TSL2591_GAIN_MED, + "MEDIUM": TSL2591Gain.TSL2591_GAIN_MED, + "400X": TSL2591Gain.TSL2591_GAIN_HIGH, + "HIGH": TSL2591Gain.TSL2591_GAIN_HIGH, + "9500X": TSL2591Gain.TSL2591_GAIN_MAX, + "MAX": TSL2591Gain.TSL2591_GAIN_MAX, + "MAXIMUM": TSL2591Gain.TSL2591_GAIN_MAX, +} + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +TSL2591Component = tsl2591_ns.class_( + "TSL2591Component", cg.PollingComponent, i2c.I2CDevice +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TSL2591Component), + cv.Optional(CONF_INFRARED): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VISIBLE): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FULL_SPECTRUM): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CALCULATED_LUX): sensor.sensor_schema( + UNIT_LUX, + ICON_BRIGHTNESS_6, + 4, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_NAME, default="TLS2591"): cv.string, + cv.Optional(CONF_GAIN, default="MEDIUM"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_POWER_SAVE_MODE, default=True): cv.boolean, + cv.Optional(CONF_DEVICE_FACTOR, default=53.0): cv.float_with_unit( + "device_factor", "", True + ), + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=7.7): cv.float_with_unit( + "glass_attenuation_factor", "", True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)) +) + + +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_FULL_SPECTRUM in config: + conf = config[CONF_FULL_SPECTRUM] + sens = await sensor.new_sensor(conf) + cg.add(var.set_full_spectrum_sensor(sens)) + + if CONF_INFRARED in config: + conf = config[CONF_INFRARED] + sens = await sensor.new_sensor(conf) + cg.add(var.set_infrared_sensor(sens)) + + if CONF_VISIBLE in config: + conf = config[CONF_VISIBLE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_visible_sensor(sens)) + + if CONF_CALCULATED_LUX in config: + conf = config[CONF_CALCULATED_LUX] + sens = await sensor.new_sensor(conf) + cg.add(var.set_calculated_lux_sensor(sens)) + + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add( + var.set_device_and_glass_attenuation_factors( + config[CONF_DEVICE_FACTOR], config[CONF_GLASS_ATTENUATION_FACTOR] + ) + ) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp new file mode 100644 index 0000000000..7755437de2 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -0,0 +1,370 @@ +#include "tsl2591.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tsl2591 { + +static const char *const TAG = "tsl2591.sensor"; + +// Various constants used in TSL2591 register manipulation +#define TSL2591_COMMAND_BIT (0xA0) // 1010 0000: bits 7 and 5 for 'command, normal' +#define TSL2591_ENABLE_POWERON (0x01) // Flag for ENABLE register, to enable +#define TSL2591_ENABLE_POWEROFF (0x00) // Flag for ENABLE register, to disable +#define TSL2591_ENABLE_AEN (0x02) // Flag for ENABLE register, to turn on ADCs + +// TSL2591 registers from the datasheet. We only define what we use. +#define TSL2591_REGISTER_ENABLE (0x00) +#define TSL2591_REGISTER_CONTROL (0x01) +#define TSL2591_REGISTER_DEVICE_ID (0x12) +#define TSL2591_REGISTER_STATUS (0x13) +#define TSL2591_REGISTER_CHAN0_LOW (0x14) +#define TSL2591_REGISTER_CHAN0_HIGH (0x15) +#define TSL2591_REGISTER_CHAN1_LOW (0x16) +#define TSL2591_REGISTER_CHAN1_HIGH (0x17) + +void TSL2591Component::enable() { + // Enable the device by setting the control bit to 0x01. Also turn on ADCs. + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWERON | TSL2591_ENABLE_AEN)) { + ESP_LOGE(TAG, "Failed I2C write during enable()"); + } +} + +void TSL2591Component::disable() { + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWEROFF)) { + ESP_LOGE(TAG, "Failed I2C write during disable()"); + } +} + +void TSL2591Component::disable_if_power_saving_() { + if (this->power_save_mode_enabled_) { + this->disable(); + } +} + +void TSL2591Component::setup() { + uint8_t address = this->address_; + ESP_LOGI(TAG, "Setting up TSL2591 sensor at I2C address 0x%02X", address); + uint8_t id; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) { + ESP_LOGE(TAG, "Failed I2C read during setup()"); + this->mark_failed(); + return; + } + if (id != 0x50) { + ESP_LOGE(TAG, + "Could not find the TSL2591 sensor. The ID register of the device at address 0x%02X reported 0x%02X " + "instead of 0x50.", + address, id); + this->mark_failed(); + return; + } + this->set_integration_time_and_gain(this->integration_time_, this->gain_); + this->disable_if_power_saving_(); +} + +void TSL2591Component::dump_config() { + ESP_LOGCONFIG(TAG, "TSL2591:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TSL2591 failed earlier, during setup"); + return; + } + + ESP_LOGCONFIG(TAG, " Name: %s", this->name_); + TSL2591Gain raw_gain = this->gain_; + int gain = 0; + std::string gain_word = "unknown"; + switch (raw_gain) { + case TSL2591_GAIN_LOW: + gain = 1; + gain_word = "low"; + break; + case TSL2591_GAIN_MED: + gain = 25; + gain_word = "medium"; + break; + case TSL2591_GAIN_HIGH: + gain = 400; + gain_word = "high"; + break; + case TSL2591_GAIN_MAX: + gain = 9500; + gain_word = "maximum"; + break; + } + ESP_LOGCONFIG(TAG, " Gain: %dx (%s)", gain, gain_word.c_str()); + TSL2591IntegrationTime raw_timing = this->integration_time_; + int timing_ms = (1 + raw_timing) * 100; + ESP_LOGCONFIG(TAG, " Integration Time: %d ms", timing_ms); + ESP_LOGCONFIG(TAG, " Power save mode enabled: %s", ONOFF(this->power_save_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Device factor: %f", this->device_factor_); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + LOG_SENSOR(" ", "Full spectrum:", this->full_spectrum_sensor_); + LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_); + LOG_SENSOR(" ", "Visible:", this->visible_sensor_); + LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_); + + LOG_UPDATE_INTERVAL(this); +} + +void TSL2591Component::process_update_() { + uint32_t combined = this->get_combined_illuminance(); + uint16_t visible = this->get_illuminance(TSL2591_SENSOR_CHANNEL_VISIBLE, combined); + uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined); + uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); + float lux = this->get_calculated_lux(full, infrared); + ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared, + visible, lux); + if (this->full_spectrum_sensor_ != nullptr) { + this->full_spectrum_sensor_->publish_state(full); + } + if (this->infrared_sensor_ != nullptr) { + this->infrared_sensor_->publish_state(infrared); + } + if (this->visible_sensor_ != nullptr) { + this->visible_sensor_->publish_state(visible); + } + if (this->calculated_lux_sensor_ != nullptr) { + this->calculated_lux_sensor_->publish_state(lux); + } + this->status_clear_warning(); +} + +#define interval_name "tsl2591_interval_for_update" + +void TSL2591Component::interval_function_for_update_() { + if (!this->is_adc_valid()) { + uint64_t now = millis(); + ESP_LOGD(TAG, "Elapsed %3llu ms; still waiting for valid ADC", (now - this->interval_start_)); + if (now > this->interval_timeout_) { + ESP_LOGW(TAG, "Interval timeout for TSL2591 '%s' expired before ADCs became valid.", this->name_); + this->cancel_interval(interval_name); + } + return; + } + this->cancel_interval(interval_name); + this->process_update_(); +} + +void TSL2591Component::update() { + if (!is_failed()) { + if (this->power_save_mode_enabled_) { + // we enabled it here, else ADC will never become valid + // but actually doing the reads will disable device if needed + this->enable(); + } + if (this->is_adc_valid()) { + this->process_update_(); + } else { + this->interval_start_ = millis(); + this->interval_timeout_ = this->interval_start_ + 620; + this->set_interval(interval_name, 100, [this] { this->interval_function_for_update_(); }); + } + } +} + +void TSL2591Component::set_infrared_sensor(sensor::Sensor *infrared_sensor) { + this->infrared_sensor_ = infrared_sensor; +} + +void TSL2591Component::set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; } + +void TSL2591Component::set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor) { + this->full_spectrum_sensor_ = full_spectrum_sensor; +} + +void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor) { + this->calculated_lux_sensor_ = calculated_lux_sensor; +} + +void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) { + this->integration_time_ = integration_time; +} + +void TSL2591Component::set_gain(TSL2591Gain gain) { this->gain_ = gain; } + +void TSL2591Component::set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor) { + this->device_factor_ = device_factor; + this->glass_attenuation_factor_ = glass_attenuation_factor; +} + +void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain) { + this->enable(); + this->integration_time_ = integration_time; + this->gain_ = gain; + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL, + this->integration_time_ | this->gain_)) { // NOLINT + ESP_LOGE(TAG, "Failed I2C write during set_integration_time_and_gain()"); + } + // The ADC values can be confused if gain or integration time are changed in the middle of a cycle. + // So, we unconditionally disable the device to turn the ADCs off. When re-enabling, the ADCs + // will tell us when they are ready again. That avoids an initial bogus reading. + this->disable(); + if (!this->power_save_mode_enabled_) { + this->enable(); + } +} + +void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_enabled_ = enable; } + +void TSL2591Component::set_name(const char *name) { this->name_ = name; } + +float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } + +bool TSL2591Component::is_adc_valid() { + uint8_t status; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { + ESP_LOGE(TAG, "Failed I2C read during is_adc_valid()"); + return false; + } + return status & 0x01; +} + +uint32_t TSL2591Component::get_combined_illuminance() { + this->enable(); + // Wait x ms for ADC to complete and signal valid. + // The max integration time is 600ms, so that's our max delay. + // (But we use 620ms as a bit of slack.) + // We'll do mini-delays and break out as soon as the ADC is good. + bool avalid; + const uint8_t mini_delay = 100; + for (uint16_t d = 0; d < 620; d += mini_delay) { + avalid = this->is_adc_valid(); + if (avalid) { + break; + } + // we only log this if we need any delay, since normally we don't + ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false"); + delay(mini_delay); + } + if (!avalid) { + // still not valid after a sutiable delay + // we don't mark the device as failed since it might come around in the future (probably not :-() + ESP_LOGE(TAG, "tsl2591 device '%s' did not return valid readings.", this->name_); + this->disable_if_power_saving_(); + return 0; + } + + // CHAN0 must be read before CHAN1 + // See: https://forums.adafruit.com/viewtopic.php?f=19&t=124176 + // Also, low byte must be read before high byte.. + // We read the registers in the order described in the datasheet. + uint32_t x32; + uint8_t ch0low, ch0high, ch1low, ch1high; + uint16_t ch0_16; + uint16_t ch1_16; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_LOW, &ch0low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_HIGH, &ch0high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch0_16 = (ch0high << 8) | ch0low; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_LOW, &ch1low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_HIGH, &ch1high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch1_16 = (ch1high << 8) | ch1low; + x32 = (ch1_16 << 16) | ch0_16; + + this->disable_if_power_saving_(); + return x32; +} + +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel) { + uint32_t combined = this->get_combined_illuminance(); + return this->get_illuminance(channel, combined); +} +// logic cloned from Adafruit TSL2591 library +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance) { + if (channel == TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM) { + // Reads two byte value from channel 0 (visible + infrared) + return (combined_illuminance & 0xFFFF); + } else if (channel == TSL2591_SENSOR_CHANNEL_INFRARED) { + // Reads two byte value from channel 1 (infrared) + return (combined_illuminance >> 16); + } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { + // Reads all and subtracts out the infrared + return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + } + // unknown channel! + ESP_LOGE(TAG, "TSL2591Component::get_illuminance() caller requested an unknown channel: %d", channel); + return 0; +} + +/** Calculates a lux value from the two TSL2591 physical sensor ADC readings. + * + * The lux calculation is copied from the Adafruit TSL2591 library. + * There is some debate about whether it is the correct lux equation to use. + * We use that lux equation because (a) it helps with a transition from + * using that Adafruit library to using this ESPHome integration, and (b) we + * don't have a definitive better idea. + * + * Since the raw ADC readings are available, you can ignore this method and + * implement your own lux equation. + * + * @param full_spectrum The ADC reading for TSL2591 channel 0. + * @param infrared The ADC reading for TSL2591 channel 1. + */ +float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infrared) { + // Check for overflow conditions first + uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535); + if ((full_spectrum == max_count) || (infrared == max_count)) { + // Signal an overflow + ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_); + return -1.0F; + } + + if ((full_spectrum == 0) && (infrared == 0)) { + // trivial conversion; avoids divide by 0 + ESP_LOGW(TAG, "Zero reading on both TSL2591 (%s) sensors. Is the device having a problem?", this->name_); + return 0.0F; + } + + float atime = 100.F + (this->integration_time_ * 100); + + float again; + switch (this->gain_) { + case TSL2591_GAIN_LOW: + again = 1.0F; + break; + case TSL2591_GAIN_MED: + again = 25.0F; + break; + case TSL2591_GAIN_HIGH: + again = 400.0F; + break; + case TSL2591_GAIN_MAX: + again = 9500.0F; + break; + default: + again = 1.0F; + break; + } + + // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly. + // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14 + // and that library code. + // They said: + // Note: This algorithm is based on preliminary coefficients + // provided by AMS and may need to be updated in the future + // However, we use gain multipliers that are more in line with the midpoints + // of ranges from the datasheet. We don't know why the other libraries + // used the values they did for HIGH and MAX. + // If cpl or full_spectrum are 0, this will return NaN due to divide by 0. + // For the curious "cpl" is counts per lux, a term used in AMS application notes. + float cpl = (atime * again) / (this->device_factor_ * this->glass_attenuation_factor_); + float lux = (((float) full_spectrum - (float) infrared)) * (1.0F - ((float) infrared / (float) full_spectrum)) / cpl; + return std::max(lux, 0.0F); +} + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h new file mode 100644 index 0000000000..19352a15c5 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tsl2591 { + +/** Enum listing all conversion/integration time settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Longer times mean more accurate results, but will take more energy/more time. + */ +enum TSL2591IntegrationTime { + TSL2591_INTEGRATION_TIME_100MS = 0b000, + TSL2591_INTEGRATION_TIME_200MS = 0b001, + TSL2591_INTEGRATION_TIME_300MS = 0b010, + TSL2591_INTEGRATION_TIME_400MS = 0b011, + TSL2591_INTEGRATION_TIME_500MS = 0b100, + TSL2591_INTEGRATION_TIME_600MS = 0b101, +}; + +/** Enum listing all gain settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Higher values are better for low light situations, but can increase noise. + */ +enum TSL2591Gain { + TSL2591_GAIN_LOW = 0b00 << 4, // 1x + TSL2591_GAIN_MED = 0b01 << 4, // 25x + TSL2591_GAIN_HIGH = 0b10 << 4, // 400x + TSL2591_GAIN_MAX = 0b11 << 4, // 9500x +}; + +/** Enum listing sensor channels. + * + * They identify the type of light to report. + */ +enum TSL2591SensorChannel { + TSL2591_SENSOR_CHANNEL_VISIBLE, + TSL2591_SENSOR_CHANNEL_INFRARED, + TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, +}; + +/// This class includes support for the TSL2591 i2c ambient light +/// sensor. The device has two distinct sensors. One is for visible +/// light plus infrared light, and the other is for infrared +/// light. They are reported as separate sensors, and the difference +/// between the values is reported as a third sensor as a convenience +/// for visible light only. +class TSL2591Component : public PollingComponent, public i2c::I2CDevice { + public: + /** Set device integration time and gain. + * + * These are set as a single I2C transaction, so you must supply values + * for both. + * + * Longer integration times provides more accurate values, but also + * means more power consumption. Higher gain values are useful for + * lower light intensities but are also subject to more noise. The + * device might use a slightly different gain multiplier than those + * indicated; see the datasheet for details. + * + * Possible values for integration_time (from enum + * TSL2591IntegrationTime) are: + * + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_100MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_200MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_300MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_400MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_500MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_600MS` + * + * Possible values for gain (from enum TSL2591Gain) are: + * + * - `esphome::tsl2591::TSL2591_GAIN_LOW` (1x) + * - `esphome::tsl2591::TSL2591_GAIN_MED` (25x) + * - `esphome::tsl2591::TSL2591_GAIN_HIGH` (400x) + * - `esphome::tsl2591::TSL2591_GAIN_MAX` (9500x) + * + * @param integration_time The new integration time. + * @param gain The new gain. + */ + void set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain); + + /** Should the device be powered down between readings? + * + * The disadvantage of powering down the device between readings + * is that you have to wait for the ADC to go through an + * integration cycle before a reliable reading is available. + * This happens during ESPHome's update loop, so waiting slows + * down the entire ESP device. You should only enable this if + * you need to minimize power consumption and you can tolerate + * that delay. Otherwise, keep the default of disabling + * power save mode. + * + * @param enable Enable or disable power save mode. + */ + void set_power_save_mode(bool enable); + + /** Sets the name for this instance of the device. + * + * @param name The user-friendly name. + */ + void set_name(const char *name); + + /** Sets the device and glass attenuation factors. + * + * The lux equation, used to calculate the lux from the ADC readings, + * involves a scaling coefficient that is the product of a device + * factor (specific to the type of device being used) and a glass + * attenuation factor (specific to whatever glass or plastic cover + * is installed in front of the light sensors. + * + * AMS does not publish the device factor for the TSL2591. In the + * datasheet for the earlier TSL2571 and in application notes, they + * use the value 53, so we use that as the default. + * + * The glass attenuation factor depends on factors external to the + * TSL2591 and is best obtained through experimental measurements. + * The Adafruit TSL2591 library use a value of ~7.7, which we use as + * a default. Waveshare uses a value of ~14.4. Presumably, those + * factors are appropriate to the breakout boards from those vendors, + * but we have not verified that. + * + * @param device_factor The device factor. + * @param glass_attenuation_factor The glass attenuation factor. + */ + void set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor); + + /** Calculates and returns a lux value based on the ADC readings. + * + * @param full_spectrum The ADC reading for the full spectrum sensor. + * @param infrared The ADC reading for the infrared sensor. + */ + float get_calculated_lux(uint16_t full_spectrum, uint16_t infrared); + + /** Get the combined illuminance value. + * + * This is encoded into a 32 bit value. The high 16 bits are the value of the + * infrared sensor. The low 16 bits are the sum of the combined sensor values. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + */ + uint32_t get_combined_illuminance(); + + /** Get an individual sensor channel reading. + * + * This gets an individual light sensor reading. Since it goes through + * the entire component read cycle to get one value, it's not optimal if + * you want to get all possible channel values. If you want that, first + * call `get_combined_illuminance()` and pass that value to the companion + * method with a different signature. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + * + * @param channel The sensor channel of interest. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel); + + /** Get an individual sensor channel reading from combined illuminance. + * + * This gets an individual light sensor reading from a combined illuminance + * value, which you would obtain from calling `getCombinedIlluminance()`. + * This method does not communicate with the sensor at all. It's strictly + * local calculations, so it is efficient if you call it multiple times. + * + * @param channel The sensor channel of interest. + * @param combined_illuminance The previously obtained combined illuminance value. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance); + + /** Are the device ADC values valid? + * + * Useful for scripting. This should be checked before calling update(). + * It asks the TSL2591 if the ADC has completed an integration cycle + * and has reliable values in the device registers. If you call update() + * before the ADC values are valid, you may cause a general delay in + * the ESPHome update loop. + * + * It should take no more than the configured integration time for + * the ADC values to become valid after the TSL2591 device is enabled. + */ + bool is_adc_valid(); + + /** Powers on the TSL2591 device and enables its sensors. + * + * You only need to call this if you have disabled the device. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void enable(); + /** Powers off the TSL2591 device. + * + * You can call this from an ESPHome script if you are explicitly + * controlling TSL2591 power consumption. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void disable(); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these. They're for ESPHome integration use.) + /** Used by ESPHome framework. */ + void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor); + /** Used by ESPHome framework. */ + void set_infrared_sensor(sensor::Sensor *infrared_sensor); + /** Used by ESPHome framework. */ + void set_visible_sensor(sensor::Sensor *visible_sensor); + /** Used by ESPHome framework. */ + void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_integration_time(TSL2591IntegrationTime integration_time); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_gain(TSL2591Gain gain); + /** Used by ESPHome framework. */ + void setup() override; + /** 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; + + protected: + const char *name_; + sensor::Sensor *full_spectrum_sensor_; + sensor::Sensor *infrared_sensor_; + sensor::Sensor *visible_sensor_; + sensor::Sensor *calculated_lux_sensor_; + TSL2591IntegrationTime integration_time_; + TSL2591Gain gain_; + bool power_save_mode_enabled_; + float device_factor_; + float glass_attenuation_factor_; + uint64_t interval_start_; + uint64_t interval_timeout_; + void disable_if_power_saving_(); + void process_update_(); + void interval_function_for_update_(); +}; + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 94dd014d8e..59749a4fa7 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 6e3e68ea7a..21c7b02740 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -8,7 +8,8 @@ static const char *const TAG = "ttp229_lsf"; void TTP229LSFComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up ttp229..."); - if (!this->parent_->raw_request_from(this->address_, 2)) { + uint8_t data[2]; + if (this->read(data, 2) != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; @@ -28,10 +29,11 @@ void TTP229LSFComponent::dump_config() { } void TTP229LSFComponent::loop() { uint16_t touched = 0; - if (!this->parent_->raw_receive_16(this->address_, &touched, 1)) { + if (this->read(reinterpret_cast(&touched), 2) != i2c::ERROR_OK) { this->status_set_warning(); return; } + touched = i2c::i2ctohs(touched); this->status_clear_warning(); touched = reverse_bits_16(touched); for (auto *channel : this->channels_) { diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 06c80964ee..275a87edd3 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -1,3 +1,4 @@ +from esphome import pins from esphome.components import climate import esphome.config_validation as cv import esphome.codegen as cg @@ -15,11 +16,15 @@ 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_HEATING_STATE_PIN = "heating_state_pin" +CONF_COOLING_STATE_PIN = "cooling_state_pin" CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" 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" TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) @@ -69,13 +74,28 @@ def validate_temperature_multipliers(value): def validate_active_state_values(value): if CONF_ACTIVE_STATE_DATAPOINT not in value: - return value - 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" + 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" + ) + ) + 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 @@ -91,16 +111,23 @@ CONFIG_SCHEMA = cv.All( 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_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, cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, 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, } ).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, ) @@ -118,14 +145,29 @@ async def to_code(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 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] + ) + ) + else: + if CONF_HEATING_STATE_PIN in config: + heating_state_pin = await cg.gpio_pin_expression( + config[CONF_HEATING_STATE_PIN] + ) + 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: @@ -150,3 +192,7 @@ async def to_code(config): 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])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 66fdcbb472..39d4203684 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -13,7 +13,7 @@ void TuyaClimate::setup() { this->mode = climate::CLIMATE_MODE_OFF; if (datapoint.value_bool) { if (this->supports_heat_ && this->supports_cool_) { - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; } else if (this->supports_heat_) { this->mode = climate::CLIMATE_MODE_HEAT; } else if (this->supports_cool_) { @@ -31,11 +31,21 @@ 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) { - this->target_temperature = datapoint.value_int * this->target_temperature_multiplier_; - ESP_LOGV(TAG, "MCU reported target temperature is: %.1f", this->target_temperature); + this->manual_temperature_ = datapoint.value_int * this->target_temperature_multiplier_; + ESP_LOGV(TAG, "MCU reported manual target temperature is: %.1f", this->manual_temperature_); + this->compute_target_temperature_(); this->compute_state_(); this->publish_state(); }); @@ -48,29 +58,81 @@ void TuyaClimate::setup() { this->publish_state(); }); } + if (this->eco_id_.has_value()) { + this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { + this->eco_ = datapoint.value_bool; + ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_)); + this->compute_preset_(); + this->compute_target_temperature_(); + 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(); + if (heating_state != this->heating_state_) { + ESP_LOGV(TAG, "Heating state pin changed to: %s", ONOFF(heating_state)); + this->heating_state_ = heating_state; + state_changed = true; + } + } + if (this->cooling_state_pin_ != nullptr) { + bool cooling_state = this->cooling_state_pin_->digital_read(); + if (cooling_state != this->cooling_state_) { + ESP_LOGV(TAG, "Cooling state pin changed to: %s", ONOFF(cooling_state)); + this->cooling_state_ = cooling_state; + state_changed = true; + } + } + + if (state_changed) { + this->compute_state_(); + this->publish_state(); + } } void TuyaClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) { const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); - this->parent_->set_datapoint_value(*this->switch_id_, switch_state); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); } if (call.get_target_temperature().has_value()) { const float target_temperature = *call.get_target_temperature(); ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature); - this->parent_->set_datapoint_value(*this->target_temperature_id_, - (int) (target_temperature / this->target_temperature_multiplier_)); + this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, + (int) (target_temperature / this->target_temperature_multiplier_)); + } + + if (call.get_preset().has_value()) { + const climate::ClimatePreset preset = *call.get_preset(); + if (this->eco_id_.has_value()) { + const bool eco = preset == climate::CLIMATE_PRESET_ECO; + ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); + this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + } } } climate::ClimateTraits TuyaClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_action(true); + traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (this->eco_id_.has_value()) { + traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); + traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); + } return traits; } @@ -84,10 +146,30 @@ void TuyaClimate::dump_config() { ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); if (this->current_temperature_id_.has_value()) ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + LOG_PIN(" Heating State Pin: ", this->heating_state_pin_); + LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_); + if (this->eco_id_.has_value()) + ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); +} + +void TuyaClimate::compute_preset_() { + if (this->eco_) { + this->preset = climate::CLIMATE_PRESET_ECO; + } else { + this->preset = climate::CLIMATE_PRESET_NONE; + } +} + +void TuyaClimate::compute_target_temperature_() { + if (this->eco_ && this->eco_temperature_.has_value()) { + this->target_temperature = *this->eco_temperature_; + } else { + this->target_temperature = this->manual_temperature_; + } } void TuyaClimate::compute_state_() { - if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) { // if any control parameters are nan, go to OFF action (not IDLE!) this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; @@ -100,6 +182,7 @@ void TuyaClimate::compute_state_() { climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE; 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; @@ -107,6 +190,13 @@ void TuyaClimate::compute_state_() { 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; + } } else { // Fallback to active state calc based on temp and hysteresis const float temp_diff = this->target_temperature - this->current_temperature; diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index f015bc337c..ec19d05308 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -10,6 +10,7 @@ namespace tuya { class TuyaClimate : public climate::Climate, public Component { public: void setup() override; + void loop() override; void dump_config() override; void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } @@ -17,6 +18,8 @@ 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_heating_state_pin(GPIOPin *pin) { this->heating_state_pin_ = pin; } + void set_cooling_state_pin(GPIOPin *pin) { this->cooling_state_pin_ = pin; } void set_target_temperature_id(uint8_t target_temperature_id) { this->target_temperature_id_ = target_temperature_id; } @@ -29,15 +32,24 @@ class TuyaClimate : public climate::Climate, public Component { void set_target_temperature_multiplier(float temperature_multiplier) { this->target_temperature_multiplier_ = temperature_multiplier; } + 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_tuya_parent(Tuya *parent) { this->parent_ = parent; } protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. climate::ClimateTraits traits() override; + /// Re-compute the active preset of this climate controller. + void compute_preset_(); + + /// Re-compute the target temperature of this climate controller. + void compute_target_temperature_(); + /// Re-compute the state of this climate controller. void compute_state_(); @@ -51,12 +63,20 @@ class TuyaClimate : public climate::Climate, public Component { optional active_state_id_{}; optional active_state_heating_value_{}; optional active_state_cooling_value_{}; + GPIOPin *heating_state_pin_{nullptr}; + GPIOPin *cooling_state_pin_{nullptr}; optional target_temperature_id_{}; optional current_temperature_id_{}; float current_temperature_multiplier_{1.0f}; float target_temperature_multiplier_{1.0f}; float hysteresis_{1.0f}; + optional eco_id_{}; + optional eco_temperature_{}; uint8_t active_state_; + bool heating_state_{false}; + bool cooling_state_{false}; + float manual_temperature_; + bool eco_; }; } // namespace tuya diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py new file mode 100644 index 0000000000..5a654841f7 --- /dev/null +++ b/esphome/components/tuya/cover/__init__.py @@ -0,0 +1,52 @@ +from esphome.components import cover +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_MIN_VALUE, + CONF_MAX_VALUE, +) +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] + +CONF_POSITION_DATAPOINT = "position_datapoint" +CONF_INVERT_POSITION = "invert_position" + +TuyaCover = tuya_ns.class_("TuyaCover", cover.Cover, cg.Component) + + +def validate_range(config): + if config[CONF_MIN_VALUE] > config[CONF_MAX_VALUE]: + raise cv.Invalid( + f"min_value ({config[CONF_MIN_VALUE]}) cannot be greater than max_value ({config[CONF_MAX_VALUE]})" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cover.COVER_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaCover), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_POSITION_DATAPOINT): cv.uint8_t, + 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, + }, + ).extend(cv.COMPONENT_SCHEMA), + validate_range, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + + cg.add(var.set_position_id(config[CONF_POSITION_DATAPOINT])) + 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])) + 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 new file mode 100644 index 0000000000..7da1312938 --- /dev/null +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -0,0 +1,58 @@ +#include "esphome/core/log.h" +#include "tuya_cover.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.cover"; + +void TuyaCover::setup() { + this->value_range_ = this->max_value_ - this->min_value_; + if (this->position_id_.has_value()) { + this->parent_->register_listener(*this->position_id_, [this](const TuyaDatapoint &datapoint) { + auto pos = float(datapoint.value_uint - this->min_value_) / this->value_range_; + if (this->invert_position_) + pos = 1.0f - pos; + this->position = pos; + this->publish_state(); + }); + } +} + +void TuyaCover::control(const cover::CoverCall &call) { + if (call.get_stop()) { + auto pos = this->position; + if (this->invert_position_) + pos = 1.0f - pos; + auto position_int = static_cast(pos * this->value_range_); + position_int = position_int + this->min_value_; + + parent_->set_integer_datapoint_value(*this->position_id_, position_int); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + if (this->invert_position_) + pos = 1.0f - pos; + auto position_int = static_cast(pos * this->value_range_); + position_int = position_int + this->min_value_; + + parent_->set_integer_datapoint_value(*this->position_id_, position_int); + } + + this->publish_state(); +} + +void TuyaCover::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Cover:"); + if (this->position_id_.has_value()) + ESP_LOGCONFIG(TAG, " Position has datapoint ID %u", *this->position_id_); +} + +cover::CoverTraits TuyaCover::get_traits() { + auto traits = cover::CoverTraits(); + traits.set_supports_position(true); + return traits; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/cover/tuya_cover.h b/esphome/components/tuya/cover/tuya_cover.h new file mode 100644 index 0000000000..c3b0c3e069 --- /dev/null +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace tuya { + +class TuyaCover : public cover::Cover, public Component { + public: + void setup() override; + void dump_config() override; + void set_position_id(uint8_t dimmer_id) { this->position_id_ = dimmer_id; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + 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; } + + protected: + void control(const cover::CoverCall &call) override; + cover::CoverTraits get_traits() override; + + Tuya *parent_; + optional position_id_{}; + uint32_t min_value_; + uint32_t max_value_; + uint32_t value_range_; + bool invert_position_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 56efcf2d77..d0c8809564 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -67,22 +67,26 @@ void TuyaFan::dump_config() { void TuyaFan::write_state() { if (this->switch_id_.has_value()) { ESP_LOGV(TAG, "Setting switch: %s", ONOFF(this->fan_->state)); - this->parent_->set_datapoint_value(*this->switch_id_, this->fan_->state); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, this->fan_->state); } if (this->oscillation_id_.has_value()) { ESP_LOGV(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); - this->parent_->set_datapoint_value(*this->oscillation_id_, this->fan_->oscillating); + this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, this->fan_->oscillating); } if (this->direction_id_.has_value()) { bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; ESP_LOGV(TAG, "Setting reverse direction: %s", ONOFF(enable)); - this->parent_->set_datapoint_value(*this->direction_id_, enable); + this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value()) { ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed); - this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed); + this->parent_->set_enum_datapoint_value(*this->speed_id_, this->fan_->speed - 1); } } +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float TuyaFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } + } // namespace tuya } // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index a24e7a218e..e96770d8c3 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -11,6 +11,7 @@ class TuyaFan : public Component { public: TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {} void setup() override; + float get_setup_priority() const override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 979082d636..b983e3f84e 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_SWITCH_DATAPOINT, CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_COLOR_INTERLOCK, ) from .. import tuya_ns, CONF_TUYA_ID, Tuya @@ -18,7 +19,10 @@ DEPENDENCIES = ["tuya"] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" CONF_MIN_VALUE_DATAPOINT = "min_value_datapoint" CONF_COLOR_TEMPERATURE_DATAPOINT = "color_temperature_datapoint" +CONF_COLOR_TEMPERATURE_INVERT = "color_temperature_invert" CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value" +CONF_RGB_DATAPOINT = "rgb_datapoint" +CONF_HSV_DATAPOINT = "hsv_datapoint" TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) @@ -30,9 +34,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Exclusive(CONF_RGB_DATAPOINT, "color"): cv.uint8_t, + cv.Exclusive(CONF_HSV_DATAPOINT, "color"): cv.uint8_t, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" ): cv.uint8_t, + cv.Optional(CONF_COLOR_TEMPERATURE_INVERT, default=False): cv.boolean, cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, cv.Optional(CONF_COLOR_TEMPERATURE_MAX_VALUE): cv.int_, @@ -50,7 +58,12 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA), - cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT), + cv.has_at_least_one_key( + CONF_DIMMER_DATAPOINT, + CONF_SWITCH_DATAPOINT, + CONF_RGB_DATAPOINT, + CONF_HSV_DATAPOINT, + ), ) @@ -65,8 +78,14 @@ async def to_code(config): cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT])) if CONF_SWITCH_DATAPOINT in config: cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_RGB_DATAPOINT in config: + cg.add(var.set_rgb_id(config[CONF_RGB_DATAPOINT])) + elif CONF_HSV_DATAPOINT in config: + cg.add(var.set_hsv_id(config[CONF_HSV_DATAPOINT])) if CONF_COLOR_TEMPERATURE_DATAPOINT in config: cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT])) + cg.add(var.set_color_temperature_invert(config[CONF_COLOR_TEMPERATURE_INVERT])) + cg.add( var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) ) @@ -83,5 +102,7 @@ async def to_code(config): config[CONF_COLOR_TEMPERATURE_MAX_VALUE] ) ) + + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index d7e3561328..133ee1e557 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "tuya_light.h" +#include "esphome/core/helpers.h" namespace esphome { namespace tuya { @@ -9,10 +10,14 @@ static const char *const TAG = "tuya.light"; void TuyaLight::setup() { if (this->color_temperature_id_.has_value()) { this->parent_->register_listener(*this->color_temperature_id_, [this](const TuyaDatapoint &datapoint) { + auto datapoint_value = datapoint.value_uint; + if (this->color_temperature_invert_) { + datapoint_value = this->color_temperature_max_value_ - datapoint_value; + } auto call = this->state_->make_call(); call.set_color_temperature(this->cold_white_temperature_ + (this->warm_white_temperature_ - this->cold_white_temperature_) * - (float(datapoint.value_uint) / float(this->color_temperature_max_value_))); + (float(datapoint_value) / this->color_temperature_max_value_)); call.perform(); }); } @@ -30,8 +35,33 @@ void TuyaLight::setup() { call.perform(); }); } + if (rgb_id_.has_value()) { + this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { + auto red = parse_hex(datapoint.value_string, 0, 2); + auto green = parse_hex(datapoint.value_string, 2, 2); + auto blue = parse_hex(datapoint.value_string, 4, 2); + if (red.has_value() && green.has_value() && blue.has_value()) { + auto call = this->state_->make_call(); + call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); + call.perform(); + } + }); + } else if (hsv_id_.has_value()) { + this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { + auto hue = parse_hex(datapoint.value_string, 0, 4); + auto saturation = parse_hex(datapoint.value_string, 4, 4); + auto value = parse_hex(datapoint.value_string, 8, 4); + if (hue.has_value() && saturation.has_value() && value.has_value()) { + float red, green, blue; + hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); + auto call = this->state_->make_call(); + call.set_rgb(red, green, blue); + call.perform(); + } + }); + } if (min_value_datapoint_id_.has_value()) { - parent_->set_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); + parent_->set_integer_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); } } @@ -41,15 +71,37 @@ void TuyaLight::dump_config() { ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); if (this->switch_id_.has_value()) ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->rgb_id_.has_value()) + ESP_LOGCONFIG(TAG, " RGB has datapoint ID %u", *this->rgb_id_); + else if (this->hsv_id_.has_value()) + ESP_LOGCONFIG(TAG, " HSV has datapoint ID %u", *this->hsv_id_); } light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); - traits.set_supports_brightness(this->dimmer_id_.has_value()); - traits.set_supports_color_temperature(this->color_temperature_id_.has_value()); - if (this->color_temperature_id_.has_value()) { + if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { + if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); + else + traits.set_supported_color_modes( + {light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + } else + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); + } else if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->dimmer_id_.has_value()) { + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + } else + traits.set_supported_color_modes({light::ColorMode::RGB}); + } else if (this->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + } else { + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); } return traits; } @@ -57,35 +109,64 @@ light::LightTraits TuyaLight::get_traits() { void TuyaLight::setup_state(light::LightState *state) { state_ = state; } void TuyaLight::write_state(light::LightState *state) { - float brightness; - state->current_values_as_brightness(&brightness); + float red = 0.0f, green = 0.0f, blue = 0.0f; + float color_temperature = 0.0f, brightness = 0.0f; - if (brightness == 0.0f) { - // turning off, first try via switch (if exists), then dimmer - if (switch_id_.has_value()) { - parent_->set_datapoint_value(*this->switch_id_, false); - } else if (dimmer_id_.has_value()) { - parent_->set_datapoint_value(*this->dimmer_id_, 0); + if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_temperature_id_.has_value()) { + state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &brightness); + } else if (this->dimmer_id_.has_value()) { + state->current_values_as_rgbw(&red, &green, &blue, &brightness); + } else { + state->current_values_as_rgb(&red, &green, &blue); } + } else if (this->color_temperature_id_.has_value()) { + state->current_values_as_ct(&color_temperature, &brightness); + } else { + state->current_values_as_brightness(&brightness); + } + + if (!state->current_values.is_on() && this->switch_id_.has_value()) { + parent_->set_boolean_datapoint_value(*this->switch_id_, false); return; } - if (this->color_temperature_id_.has_value()) { - uint32_t color_temp_int = - static_cast(this->color_temperature_max_value_ * - (state->current_values.get_color_temperature() - this->cold_white_temperature_) / - (this->warm_white_temperature_ - this->cold_white_temperature_)); - parent_->set_datapoint_value(*this->color_temperature_id_, color_temp_int); + 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_); + if (this->color_temperature_invert_) { + color_temp_int = this->color_temperature_max_value_ - color_temp_int; + } + parent_->set_integer_datapoint_value(*this->color_temperature_id_, color_temp_int); + } + + if (this->dimmer_id_.has_value()) { + auto brightness_int = static_cast(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + parent_->set_integer_datapoint_value(*this->dimmer_id_, brightness_int); + } } - auto brightness_int = static_cast(brightness * this->max_value_); - brightness_int = std::max(brightness_int, this->min_value_); - - if (this->dimmer_id_.has_value()) { - parent_->set_datapoint_value(*this->dimmer_id_, brightness_int); + if (brightness == 0.0f || !color_interlock_) { + if (this->rgb_id_.has_value()) { + char buffer[7]; + sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); + std::string rgb_value = buffer; + this->parent_->set_string_datapoint_value(*this->rgb_id_, rgb_value); + } else if (this->hsv_id_.has_value()) { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[13]; + sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); + std::string hsv_value = buffer; + this->parent_->set_string_datapoint_value(*this->hsv_id_, hsv_value); + } } + if (this->switch_id_.has_value()) { - parent_->set_datapoint_value(*this->switch_id_, true); + parent_->set_boolean_datapoint_value(*this->switch_id_, true); } } diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index 72422bc9e7..3d9f25271c 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -16,7 +16,12 @@ class TuyaLight : public Component, public light::LightOutput { this->min_value_datapoint_id_ = min_value_datapoint_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_rgb_id(uint8_t rgb_id) { this->rgb_id_ = rgb_id; } + void set_hsv_id(uint8_t hsv_id) { this->hsv_id_ = hsv_id; } void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_id; } + void set_color_temperature_invert(bool color_temperature_invert) { + this->color_temperature_invert_ = color_temperature_invert; + } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void set_min_value(uint32_t min_value) { min_value_ = min_value; } void set_max_value(uint32_t max_value) { max_value_ = max_value; } @@ -29,6 +34,8 @@ class TuyaLight : public Component, public light::LightOutput { void set_warm_white_temperature(float warm_white_temperature) { this->warm_white_temperature_ = warm_white_temperature; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } + light::LightTraits get_traits() override; void setup_state(light::LightState *state) override; void write_state(light::LightState *state) override; @@ -41,12 +48,16 @@ class TuyaLight : public Component, public light::LightOutput { optional dimmer_id_{}; optional min_value_datapoint_id_{}; optional switch_id_{}; + optional rgb_id_{}; + optional hsv_id_{}; optional color_temperature_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; uint32_t color_temperature_max_value_ = 255; float cold_white_temperature_; float warm_white_temperature_; + bool color_temperature_invert_{false}; + bool color_interlock_{false}; light::LightState *state_{nullptr}; }; diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp index 8cd09fb01c..cbd794b001 100644 --- a/esphome/components/tuya/switch/tuya_switch.cpp +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -15,7 +15,7 @@ void TuyaSwitch::setup() { void TuyaSwitch::write_state(bool state) { ESP_LOGV(TAG, "Setting switch %u: %s", this->switch_id_, ONOFF(state)); - this->parent_->set_datapoint_value(this->switch_id_, state); + this->parent_->set_boolean_datapoint_value(this->switch_id_, state); this->publish_state(state); } diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 540a925879..4f65fa7118 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,16 +1,18 @@ #include "tuya.h" #include "esphome/core/log.h" -#include "esphome/core/util.h" +#include "esphome/components/network/util.h" #include "esphome/core/helpers.h" +#include "esphome/core/util.h" namespace esphome { namespace tuya { static const char *const TAG = "tuya"; -static const int COMMAND_DELAY = 50; +static const int COMMAND_DELAY = 10; +static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { - this->set_interval("heartbeat", 10000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); } void Tuya::loop() { @@ -113,11 +115,19 @@ void Tuya::handle_char_(uint8_t c) { this->rx_message_.push_back(c); if (!this->validate_message_()) { this->rx_message_.clear(); + } else { + this->last_rx_char_timestamp_ = millis(); } } void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { - switch ((TuyaCommandType) command) { + TuyaCommandType command_type = (TuyaCommandType) command; + + if (this->expected_response_.has_value() && this->expected_response_ == command_type) { + this->expected_response_.reset(); + } + + switch (command_type) { case TuyaCommandType::HEARTBEAT: ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); this->protocol_version_ = version; @@ -156,7 +166,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->gpio_reset_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { - // If mcu returned status gpio, then we can ommit sending wifi state + // If mcu returned status gpio, then we can omit sending wifi state if (this->gpio_status_ != -1) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); @@ -231,8 +241,10 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { size_t data_size = (buffer[2] << 8) + buffer[3]; const uint8_t *data = buffer + 4; size_t data_len = len - 4; - if (data_size != data_len) { - ESP_LOGW(TAG, "Datapoint %u is not expected size", datapoint.id); + if (data_size > data_len) { + ESP_LOGW(TAG, "Datapoint %u has extra bytes that will be ignored (%zu > %zu)", datapoint.id, data_size, data_len); + } else if (data_size < data_len) { + ESP_LOGW(TAG, "Datapoint %u is truncated and cannot be parsed (%zu < %zu)", datapoint.id, data_size, data_len); return; } datapoint.len = data_len; @@ -288,7 +300,7 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); break; default: - ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, datapoint.type); + ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); return; } @@ -316,6 +328,25 @@ void Tuya::send_raw_command_(TuyaCommand command) { uint8_t version = 0; this->last_command_timestamp_ = millis(); + switch (command.cmd) { + case TuyaCommandType::HEARTBEAT: + this->expected_response_ = TuyaCommandType::HEARTBEAT; + break; + case TuyaCommandType::PRODUCT_QUERY: + this->expected_response_ = TuyaCommandType::PRODUCT_QUERY; + break; + case TuyaCommandType::CONF_QUERY: + this->expected_response_ = TuyaCommandType::CONF_QUERY; + break; + case TuyaCommandType::DATAPOINT_DELIVER: + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + break; + case TuyaCommandType::DATAPOINT_QUERY: + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + break; + default: + break; + } ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), version, hexencode(command.payload).c_str(), static_cast(this->init_state_)); @@ -331,9 +362,20 @@ void Tuya::send_raw_command_(TuyaCommand command) { } void Tuya::process_command_queue_() { - uint32_t delay = millis() - this->last_command_timestamp_; - // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly - if (delay > COMMAND_DELAY && !command_queue_.empty()) { + uint32_t now = millis(); + uint32_t delay = now - this->last_command_timestamp_; + + if (now - this->last_rx_char_timestamp_ > RECEIVE_TIMEOUT) { + this->rx_message_.clear(); + } + + if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { + this->expected_response_.reset(); + } + + // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly + if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && + !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); this->command_queue_.erase(command_queue_.begin()); } @@ -345,12 +387,12 @@ void Tuya::send_command_(const TuyaCommand &command) { } void Tuya::send_empty_command_(TuyaCommandType command) { - send_command_(TuyaCommand{.cmd = command, .payload = std::vector{0x04}}); + send_command_(TuyaCommand{.cmd = command, .payload = std::vector{}}); } void Tuya::send_wifi_status_() { uint8_t status = 0x02; - if (network_is_connected()) { + if (network::is_connected()) { status = 0x03; // Protocol version 3 also supports specifying when connected to "the cloud" @@ -398,20 +440,79 @@ void Tuya::send_local_time_() { } #endif -void Tuya::set_datapoint_value(uint8_t datapoint_id, uint32_t value) { +void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, hexencode(value).c_str()); + optional datapoint = this->get_datapoint_(datapoint_id); + if (!datapoint.has_value()) { + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != TuyaDatapointType::RAW) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); + return; + } else if (datapoint->value_raw == value) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value); +} + +void Tuya::set_boolean_datapoint_value(uint8_t datapoint_id, bool value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1); +} + +void Tuya::set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4); +} + +void Tuya::set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) { + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str()); + optional datapoint = this->get_datapoint_(datapoint_id); + if (!datapoint.has_value()) { + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != TuyaDatapointType::STRING) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); + return; + } else if (datapoint->value_string == value) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + std::vector data; + for (char const &c : value) { + data.push_back(c); + } + this->send_datapoint_command_(datapoint_id, TuyaDatapointType::STRING, data); +} + +void Tuya::set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1); +} + +void Tuya::set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BITMASK, value, length); +} + +optional Tuya::get_datapoint_(uint8_t datapoint_id) { + for (auto &datapoint : this->datapoints_) + if (datapoint.id == datapoint_id) + return datapoint; + return {}; +} + +void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value, + uint8_t length) { ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { - ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id); + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != datapoint_type) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); return; - } - if (datapoint->value_uint == value) { + } else if (datapoint->value_uint == value) { ESP_LOGV(TAG, "Not sending unchanged value"); return; } std::vector data; - switch (datapoint->len) { + switch (length) { case 4: data.push_back(value >> 24); data.push_back(value >> 16); @@ -421,34 +522,10 @@ void Tuya::set_datapoint_value(uint8_t datapoint_id, uint32_t value) { data.push_back(value >> 0); break; default: - ESP_LOGE(TAG, "Unexpected datapoint length %zu", datapoint->len); + ESP_LOGE(TAG, "Unexpected datapoint length %u", length); return; } - this->send_datapoint_command_(datapoint->id, datapoint->type, data); -} - -void Tuya::set_datapoint_value(uint8_t datapoint_id, const std::string &value) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str()); - optional datapoint = this->get_datapoint_(datapoint_id); - if (!datapoint.has_value()) { - ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id); - } - if (datapoint->value_string == value) { - ESP_LOGV(TAG, "Not sending unchanged value"); - return; - } - std::vector data; - for (char const &c : value) { - data.push_back(c); - } - this->send_datapoint_command_(datapoint->id, datapoint->type, data); -} - -optional Tuya::get_datapoint_(uint8_t datapoint_id) { - for (auto &datapoint : this->datapoints_) - if (datapoint.id == datapoint_id) - return datapoint; - return {}; + this->send_datapoint_command_(datapoint_id, datapoint_type, data); } void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data) { diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 4ca4f56366..785399502b 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -17,7 +17,7 @@ enum class TuyaDatapointType : uint8_t { INTEGER = 0x02, // 4 byte STRING = 0x03, // variable length ENUM = 0x04, // 1 byte - BITMASK = 0x05, // 2 bytes + BITMASK = 0x05, // 1/2/4 bytes }; struct TuyaDatapoint { @@ -75,8 +75,12 @@ class Tuya : public Component, public uart::UARTDevice { void loop() override; void dump_config() override; void register_listener(uint8_t datapoint_id, const std::function &func); - void set_datapoint_value(uint8_t datapoint_id, uint32_t value); - void set_datapoint_value(uint8_t datapoint_id, const std::string &value); + void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); + void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); + void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); + void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value); + void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value); + void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length); #ifdef USE_TIME void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } #endif @@ -95,6 +99,8 @@ class Tuya : public Component, public uart::UARTDevice { void process_command_queue_(); void send_command_(const TuyaCommand &command); void send_empty_command_(TuyaCommandType command); + void set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, uint32_t value, + uint8_t length); void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); void send_wifi_status_(); @@ -107,12 +113,14 @@ class Tuya : public Component, public uart::UARTDevice { int gpio_status_ = -1; int gpio_reset_ = -1; uint32_t last_command_timestamp_ = 0; + uint32_t last_rx_char_timestamp_ = 0; std::string product_ = ""; std::vector listeners_; std::vector datapoints_; std::vector rx_message_; std::vector ignore_mcu_update_on_datapoints_{}; std::vector command_queue_; + optional expected_response_{}; uint8_t wifi_status_ = -1; }; diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 57c3165d16..84df82b5e6 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_WIND_SPEED, CONF_PIN, CONF_WIND_DIRECTION_DEGREES, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_KILOMETER_PER_HOUR, @@ -23,18 +22,18 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Tx20Component), cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_WEATHER_WINDY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema( - UNIT_DEGREES, ICON_SIGN_DIRECTION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE - ), - cv.Required(CONF_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SIGN_DIRECTION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index f48e29521c..6e0b6343d1 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -20,7 +20,7 @@ void Tx20Component::setup() { this->store_.pin = this->pin_->to_isr(); this->store_.reset(); - this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, CHANGE); + this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } void Tx20Component::dump_config() { ESP_LOGCONFIG(TAG, "Tx20:"); @@ -140,8 +140,8 @@ void Tx20Component::decode_and_publish_() { } } -void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { - arg->pin_state = arg->pin->digital_read(); +void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { + arg->pin_state = arg->pin.digital_read(); const uint32_t now = micros(); if (!arg->start_time) { // only detect a start if the bit is high @@ -183,7 +183,7 @@ void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { arg->start_time = now; arg->buffer_index++; } -void ICACHE_RAM_ATTR Tx20ComponentStore::reset() { +void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; buffer_index = 0; spent_time = 0; diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index 8b79deffbc..1c617d0674 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -14,7 +15,7 @@ struct Tx20ComponentStore { volatile uint32_t spent_time; volatile bool tx20_available; volatile bool pin_state; - ISRInternalGPIOPin *pin; + ISRInternalGPIOPin pin; void reset(); static void gpio_intr(Tx20ComponentStore *arg); @@ -26,7 +27,7 @@ class Tx20Component : public Component { /// Get the textual representation of the wind direction ('N', 'SSE', ..). std::string get_wind_cardinal_direction() const; - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_wind_speed_sensor(sensor::Sensor *wind_speed_sensor) { wind_speed_sensor_ = wind_speed_sensor; } void set_wind_direction_degrees_sensor(sensor::Sensor *wind_direction_degrees_sensor) { wind_direction_degrees_sensor_ = wind_direction_degrees_sensor; @@ -41,7 +42,7 @@ class Tx20Component : public Component { void decode_and_publish_(); std::string wind_cardinal_direction_; - GPIOPin *pin_; + InternalGPIOPin *pin_; sensor::Sensor *wind_speed_sensor_; sensor::Sensor *wind_direction_degrees_sensor_; Tx20ComponentStore store_; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index aaed333e34..35af3eedf7 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,9 +1,13 @@ +from typing import Optional + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import pins, automation from esphome.const import ( CONF_BAUD_RATE, CONF_ID, + CONF_NUMBER, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, @@ -15,7 +19,16 @@ from esphome.core import CORE CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") -UARTComponent = uart_ns.class_("UARTComponent", cg.Component) +UARTComponent = uart_ns.class_("UARTComponent") + +IDFUARTComponent = uart_ns.class_("IDFUARTComponent", UARTComponent, cg.Component) +ESP32ArduinoUARTComponent = uart_ns.class_( + "ESP32ArduinoUARTComponent", UARTComponent, cg.Component +) +ESP8266UartComponent = uart_ns.class_( + "ESP8266UartComponent", UARTComponent, cg.Component +) + UARTDevice = uart_ns.class_("UARTDevice") UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) MULTI_CONF = True @@ -34,12 +47,23 @@ def validate_raw_data(value): def validate_rx_pin(value): - value = pins.input_pin(value) - if CORE.is_esp8266 and value >= 16: + value = pins.internal_gpio_input_pin_schema(value) + if CORE.is_esp8266 and value[CONF_NUMBER] >= 16: raise cv.Invalid("Pins GPIO16 and GPIO17 cannot be used as RX pins on ESP8266.") return value +def _uart_declare_type(value): + if CORE.is_esp8266: + return cv.declare_id(ESP8266UartComponent)(value) + if CORE.is_esp32: + if CORE.using_arduino: + return cv.declare_id(ESP32ArduinoUARTComponent)(value) + if CORE.using_esp_idf: + return cv.declare_id(IDFUARTComponent)(value) + raise NotImplementedError + + UARTParityOptions = uart_ns.enum("UARTParityOptions") UART_PARITY_OPTIONS = { "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, @@ -54,19 +78,19 @@ CONF_PARITY = "parity" CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.GenerateID(): cv.declare_id(UARTComponent), + cv.GenerateID(): _uart_declare_type, cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), - cv.Optional(CONF_TX_PIN): pins.output_pin, + cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RX_PIN): validate_rx_pin, cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, - cv.SplitDefault(CONF_INVERT, esp32=False): cv.All( - cv.only_on_esp32, cv.boolean - ), 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), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( UART_PARITY_OPTIONS, upper=True ), + cv.Optional(CONF_INVERT): cv.invalid( + "This option has been removed. Please instead use invert in the tx/rx pin schemas." + ), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN), @@ -81,53 +105,17 @@ async def to_code(config): cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) if CONF_TX_PIN in config: - cg.add(var.set_tx_pin(config[CONF_TX_PIN])) + tx_pin = await cg.gpio_pin_expression(config[CONF_TX_PIN]) + cg.add(var.set_tx_pin(tx_pin)) if CONF_RX_PIN in config: - cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + rx_pin = await cg.gpio_pin_expression(config[CONF_RX_PIN]) + cg.add(var.set_rx_pin(rx_pin)) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) - if CONF_INVERT in config: - cg.add(var.set_invert(config[CONF_INVERT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) -def validate_device( - name, config, item_config, baud_rate=None, require_tx=True, require_rx=True -): - if not hasattr(config, "uart_devices"): - config.uart_devices = {} - devices = config.uart_devices - - uart_config = config.get_config_by_id(item_config[CONF_UART_ID]) - - uart_id = uart_config[CONF_ID] - device = devices.setdefault(uart_id, {}) - - if require_tx: - if CONF_TX_PIN not in uart_config: - raise ValueError(f"Component {name} requires parent uart to declare tx_pin") - if CONF_TX_PIN in device: - raise ValueError( - f"Component {name} cannot use the same uart.{CONF_TX_PIN} as component {device[CONF_TX_PIN]} is already using it" - ) - device[CONF_TX_PIN] = name - - if require_rx: - if CONF_RX_PIN not in uart_config: - raise ValueError(f"Component {name} requires parent uart to declare rx_pin") - if CONF_RX_PIN in device: - raise ValueError( - f"Component {name} cannot use the same uart.{CONF_RX_PIN} as component {device[CONF_RX_PIN]} is already using it" - ) - device[CONF_RX_PIN] = name - - if baud_rate and uart_config[CONF_BAUD_RATE] != baud_rate: - raise ValueError( - f"Component {name} requires parent uart baud rate be {baud_rate}" - ) - - # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( { @@ -135,6 +123,64 @@ UART_DEVICE_SCHEMA = cv.Schema( } ) +KEY_UART_DEVICES = "uart_devices" + + +def final_validate_device_schema( + name: str, + *, + baud_rate: Optional[int] = None, + require_tx: bool = False, + require_rx: bool = False, +): + def validate_baud_rate(value): + if value != baud_rate: + raise cv.Invalid( + f"Component {name} required baud rate {baud_rate} for the uart bus" + ) + return value + + def validate_pin(opt, device): + def validator(value): + if opt in device: + raise cv.Invalid( + f"The uart {opt} is used both by {name} and {device[opt]}, " + f"but can only be used by one. Please create a new uart bus for {name}." + ) + device[opt] = name + return value + + return validator + + def validate_hub(hub_config): + hub_schema = {} + uart_id = hub_config[CONF_ID] + devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {}) + device = devices.setdefault(uart_id, {}) + + if require_tx: + hub_schema[ + cv.Required( + CONF_TX_PIN, + msg=f"Component {name} requires this uart bus to declare a tx_pin", + ) + ] = validate_pin(CONF_TX_PIN, device) + if require_rx: + hub_schema[ + cv.Required( + CONF_RX_PIN, + msg=f"Component {name} requires this 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 + 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)}, + extra=cv.ALLOW_EXTRA, + ) + async def register_uart_device(var, config): """Register a UART device, setting up all the internal values. diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 08e3395a7a..22a22e2772 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -4,75 +4,41 @@ #include "esphome/core/application.h" #include "esphome/core/defines.h" -#ifdef USE_LOGGER -#include "esphome/components/logger/logger.h" -#endif - namespace esphome { namespace uart { static const char *const TAG = "uart"; -size_t UARTComponent::write(uint8_t data) { - this->write_byte(data); - return 1; -} -int UARTComponent::read() { - uint8_t data; - if (!this->read_byte(&data)) - return -1; - return data; -} -int UARTComponent::peek() { - uint8_t data; - if (!this->peek_byte(&data)) - return -1; - return data; -} - -void UARTComponent::check_logger_conflict_() { -#ifdef USE_LOGGER - if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { - return; - } - - if (this->hw_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 -} - void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t data_bits) { - if (this->parent_->baud_rate_ != baud_rate) { + 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_->baud_rate_); + this->parent_->get_baud_rate()); } - if (this->parent_->stop_bits_ != stop_bits) { + 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, - this->parent_->stop_bits_); + this->parent_->get_stop_bits()); } - if (this->parent_->data_bits_ != data_bits) { + if (this->parent_->get_data_bits() != data_bits) { ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits, - this->parent_->data_bits_); + this->parent_->get_data_bits()); } - if (this->parent_->parity_ != parity) { - ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!", parity_to_str(parity), - parity_to_str(this->parent_->parity_)); + if (this->parent_->get_parity() != parity) { + ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!", + LOG_STR_ARG(parity_to_str(parity)), LOG_STR_ARG(parity_to_str(this->parent_->get_parity()))); } } -const char *parity_to_str(UARTParityOptions parity) { +const LogString *parity_to_str(UARTParityOptions parity) { switch (parity) { case UART_CONFIG_PARITY_NONE: - return "NONE"; + return LOG_STR("NONE"); case UART_CONFIG_PARITY_EVEN: - return "EVEN"; + return LOG_STR("EVEN"); case UART_CONFIG_PARITY_ODD: - return "ODD"; + return LOG_STR("ODD"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 6dd62070da..c368f9ed6b 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -1,128 +1,15 @@ #pragma once -#include -#include "esphome/core/esphal.h" +#include #include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "uart_component.h" namespace esphome { namespace uart { -enum UARTParityOptions { - UART_CONFIG_PARITY_NONE, - UART_CONFIG_PARITY_EVEN, - UART_CONFIG_PARITY_ODD, -}; - -const char *parity_to_str(UARTParityOptions parity); - -#ifdef ARDUINO_ARCH_ESP8266 -class ESP8266SoftwareSerial { - public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, - UARTParityOptions parity, size_t rx_buffer_size); - - uint8_t read_byte(); - uint8_t peek_byte(); - - void flush(); - - void write_byte(uint8_t data); - - int available(); - - GPIOPin *gpio_tx_pin_{nullptr}; - GPIOPin *gpio_rx_pin_{nullptr}; - - protected: - static void gpio_intr(ESP8266SoftwareSerial *arg); - - void wait_(uint32_t *wait, const uint32_t &start); - bool read_bit_(uint32_t *wait, const uint32_t &start); - void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); - - uint32_t bit_time_{0}; - uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_; - volatile size_t rx_in_pos_{0}; - size_t rx_out_pos_{0}; - uint8_t stop_bits_; - uint8_t data_bits_; - UARTParityOptions parity_; - ISRInternalGPIOPin *tx_pin_{nullptr}; - ISRInternalGPIOPin *rx_pin_{nullptr}; -}; -#endif - -class UARTComponent : public Component, public Stream { - public: - void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } - - uint32_t get_config(); - - void setup() override; - - void dump_config() override; - - void write_byte(uint8_t data); - - void write_array(const uint8_t *data, size_t len); - void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } - - void write_str(const char *str); - - bool peek_byte(uint8_t *data); - - bool read_byte(uint8_t *data); - - bool read_array(uint8_t *data, size_t len); - - int available() override; - - /// Block until all bytes have been written to the UART bus. - void flush() override; - - float get_setup_priority() const override { return setup_priority::BUS; } - - size_t write(uint8_t data) override; - int read() override; - int peek() override; - - void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } - void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } - void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } -#ifdef ARDUINO_ARCH_ESP32 - void set_invert(bool invert) { this->invert_ = invert; } -#endif - void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } - void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } - void set_parity(UARTParityOptions parity) { this->parity_ = parity; } - - protected: - void check_logger_conflict_(); - bool check_read_timeout_(size_t len = 1); - friend class UARTDevice; - - HardwareSerial *hw_serial_{nullptr}; -#ifdef ARDUINO_ARCH_ESP8266 - ESP8266SoftwareSerial *sw_serial_{nullptr}; -#endif - optional tx_pin_; - optional rx_pin_; - size_t rx_buffer_size_; -#ifdef ARDUINO_ARCH_ESP32 - bool invert_; -#endif - uint32_t baud_rate_; - uint8_t stop_bits_; - uint8_t data_bits_; - UARTParityOptions parity_; -}; - -#ifdef ARDUINO_ARCH_ESP32 -extern uint8_t next_uart_num; -#endif - -class UARTDevice : public Stream { +class UARTDevice { public: UARTDevice() = default; UARTDevice(UARTComponent *parent) : parent_(parent) {} @@ -151,13 +38,27 @@ class UARTDevice : public Stream { return res; } - int available() override { return this->parent_->available(); } + int available() { return this->parent_->available(); } - void flush() override { return this->parent_->flush(); } + void flush() { return this->parent_->flush(); } - size_t write(uint8_t data) override { return this->parent_->write(data); } - int read() override { return this->parent_->read(); } - int peek() override { return this->parent_->peek(); } + // Compat APIs + int read() { + uint8_t data; + if (!read_byte(&data)) + return -1; + return data; + } + size_t write(uint8_t data) { + write_byte(data); + return 1; + } + int peek() { + uint8_t data; + if (!peek_byte(&data)) + return -1; + return data; + } /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp new file mode 100644 index 0000000000..09b8c975ab --- /dev/null +++ b/esphome/components/uart/uart_component.cpp @@ -0,0 +1,24 @@ +#include "uart_component.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart"; + +bool UARTComponent::check_read_timeout_(size_t len) { + if (this->available() >= int(len)) + return true; + + uint32_t start_time = millis(); + while (this->available() < int(len)) { + if (millis() - start_time > 100) { + ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); + return false; + } + yield(); + } + return true; +} + +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h new file mode 100644 index 0000000000..de85cd2ca3 --- /dev/null +++ b/esphome/components/uart/uart_component.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uart { + +enum UARTParityOptions { + UART_CONFIG_PARITY_NONE, + UART_CONFIG_PARITY_EVEN, + UART_CONFIG_PARITY_ODD, +}; + +const LogString *parity_to_str(UARTParityOptions parity); + +class UARTComponent { + public: + void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } + void write_byte(uint8_t data) { this->write_array(&data, 1); }; + void write_str(const char *str) { + const auto *data = reinterpret_cast(str); + this->write_array(data, strlen(str)); + }; + + virtual void write_array(const uint8_t *data, size_t len) = 0; + + bool read_byte(uint8_t *data) { return this->read_array(data, 1); }; + virtual bool peek_byte(uint8_t *data) = 0; + virtual bool read_array(uint8_t *data, size_t len) = 0; + + /// Return available number of bytes. + virtual int available() = 0; + /// Block until all bytes have been written to the UART bus. + virtual void flush() = 0; + + void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } + void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } + void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } + + void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } + uint8_t get_stop_bits() const { return this->stop_bits_; } + void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } + uint8_t get_data_bits() const { return this->data_bits_; } + void set_parity(UARTParityOptions parity) { this->parity_ = parity; } + UARTParityOptions get_parity() const { return this->parity_; } + void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + uint32_t get_baud_rate() const { return baud_rate_; } + + protected: + virtual void check_logger_conflict() = 0; + bool check_read_timeout_(size_t len = 1); + + InternalGPIOPin *tx_pin_; + InternalGPIOPin *rx_pin_; + size_t rx_buffer_size_; + uint32_t baud_rate_; + uint8_t stop_bits_; + uint8_t data_bits_; + UARTParityOptions parity_; +}; + +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp similarity index 56% rename from esphome/components/uart/uart_esp32.cpp rename to esphome/components/uart/uart_component_esp32_arduino.cpp index 89de4c0cc1..1b1ce382f2 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -1,18 +1,21 @@ -#ifdef ARDUINO_ARCH_ESP32 -#include "uart.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" +#ifdef USE_ESP32_FRAMEWORK_ARDUINO #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "uart_component_esp32_arduino.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif namespace esphome { namespace uart { -static const char *const TAG = "uart_esp32"; -uint8_t next_uart_num = 1; +static const char *const TAG = "uart.arduino_esp32"; static const uint32_t UART_PARITY_EVEN = 0 << 0; static const uint32_t UART_PARITY_ODD = 1 << 0; -static const uint32_t UART_PARITY_EN = 1 << 1; +static const uint32_t UART_PARITY_ENABLE = 1 << 1; static const uint32_t UART_NB_BIT_5 = 0 << 2; static const uint32_t UART_NB_BIT_6 = 1 << 2; static const uint32_t UART_NB_BIT_7 = 2 << 2; @@ -21,7 +24,7 @@ static const uint32_t UART_NB_STOP_BIT_1 = 1 << 4; static const uint32_t UART_NB_STOP_BIT_2 = 3 << 4; static const uint32_t UART_TICK_APB_CLOCK = 1 << 27; -uint32_t UARTComponent::get_config() { +uint32_t ESP32ArduinoUARTComponent::get_config() { uint32_t config = 0; /* @@ -39,9 +42,9 @@ uint32_t UARTComponent::get_config() { */ if (this->parity_ == UART_CONFIG_PARITY_EVEN) - config |= UART_PARITY_EVEN | UART_PARITY_EN; + config |= UART_PARITY_EVEN | UART_PARITY_ENABLE; else if (this->parity_ == UART_CONFIG_PARITY_ODD) - config |= UART_PARITY_ODD | UART_PARITY_EN; + config |= UART_PARITY_ODD | UART_PARITY_ENABLE; switch (this->data_bits_) { case 5: @@ -68,66 +71,63 @@ uint32_t UARTComponent::get_config() { return config; } -void UARTComponent::setup() { +void ESP32ArduinoUARTComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up UART..."); // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + bool is_default_tx, is_default_rx; +#ifdef CONFIG_IDF_TARGET_ESP32C3 + is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 21; + is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 20; +#else + is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 1; + is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 3; +#endif + if (is_default_tx && is_default_rx) { this->hw_serial_ = &Serial; } else { - this->hw_serial_ = new HardwareSerial(next_uart_num++); + static uint8_t next_uart_num = 1; + this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } - int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; - int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, this->invert_); + 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; + if (tx_pin_ != nullptr && tx_pin_->is_inverted()) + invert = true; + if (rx_pin_ != nullptr && rx_pin_->is_inverted()) + invert = true; + this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } -void UARTComponent::dump_config() { +void ESP32ArduinoUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); - if (this->tx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); - } - if (this->rx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + 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", parity_to_str(this->parity_)); + 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_(); + this->check_logger_conflict(); } -void UARTComponent::write_byte(uint8_t data) { - this->hw_serial_->write(data); - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); -} -void UARTComponent::write_array(const uint8_t *data, size_t len) { +void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) { this->hw_serial_->write(data, len); for (size_t i = 0; i < len; i++) { ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); } } -void UARTComponent::write_str(const char *str) { - this->hw_serial_->write(str); - ESP_LOGVV(TAG, " Wrote \"%s\"", str); -} -bool UARTComponent::read_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - *data = this->hw_serial_->read(); - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); - return true; -} -bool UARTComponent::peek_byte(uint8_t *data) { +bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; *data = this->hw_serial_->peek(); return true; } -bool UARTComponent::read_array(uint8_t *data, size_t len) { +bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; this->hw_serial_->readBytes(data, len); @@ -137,26 +137,25 @@ bool UARTComponent::read_array(uint8_t *data, size_t len) { return true; } -bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= len) - return true; - - uint32_t start_time = millis(); - while (this->available() < len) { - if (millis() - start_time > 1000) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); - return false; - } - yield(); - } - return true; -} -int UARTComponent::available() { return this->hw_serial_->available(); } -void UARTComponent::flush() { +int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); } +void ESP32ArduinoUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing..."); this->hw_serial_->flush(); } +void ESP32ArduinoUARTComponent::check_logger_conflict() { +#ifdef USE_LOGGER + if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { + return; + } + + if (this->hw_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 // ARDUINO_ARCH_ESP32 +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h new file mode 100644 index 0000000000..c6f445ff12 --- /dev/null +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -0,0 +1,40 @@ +#pragma once + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include +#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 ESP32ArduinoUARTComponent : 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; + + uint32_t get_config(); + + protected: + void check_logger_conflict() override; + + HardwareSerial *hw_serial_{nullptr}; +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp similarity index 59% rename from esphome/components/uart/uart_esp8266.cpp rename to esphome/components/uart/uart_component_esp8266.cpp index 6f7d4c1f8b..973306cde2 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -1,15 +1,21 @@ -#ifdef ARDUINO_ARCH_ESP8266 -#include "uart.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" +#ifdef USE_ESP8266 +#include "uart_component_esp8266.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -# +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + namespace esphome { namespace uart { -static const char *const TAG = "uart_esp8266"; -uint32_t UARTComponent::get_config() { +static const char *const TAG = "uart.arduino_esp8266"; +bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t ESP8266UartComponent::get_config() { uint32_t config = 0; if (this->parity_ == UART_CONFIG_PARITY_NONE) @@ -42,65 +48,82 @@ uint32_t UARTComponent::get_config() { return config; } -void UARTComponent::setup() { +void ESP8266UartComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up UART bus..."); // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) && + (rx_pin_ == nullptr || rx_pin_->get_pin() == 3) +#ifdef USE_LOGGER + // we will use UART0 if logger isn't using it in swapped mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { + ESP8266UartComponent::serial0_in_use = true; + } else if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 15) && + (rx_pin_ == nullptr || rx_pin_->get_pin() == 13) +#ifdef USE_LOGGER + // we will use UART0 swapped if logger isn't using it in regular mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); - } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { + ESP8266UartComponent::serial0_in_use = true; + } else if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else { - this->sw_serial_ = new ESP8266SoftwareSerial(); - int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; - int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, + this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT + this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); } } -void UARTComponent::dump_config() { +void ESP8266UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); - if (this->tx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); - } - if (this->rx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + 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_); // NOLINT } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); - ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); + ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_))); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { ESP_LOGCONFIG(TAG, " Using software serial"); } - this->check_logger_conflict_(); + this->check_logger_conflict(); } -void UARTComponent::write_byte(uint8_t data) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(data); - } else { - this->sw_serial_->write_byte(data); +void ESP8266UartComponent::check_logger_conflict() { +#ifdef USE_LOGGER + if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { + return; } - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); + + if (this->hw_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 } -void UARTComponent::write_array(const uint8_t *data, size_t len) { + +void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { if (this->hw_serial_ != nullptr) { this->hw_serial_->write(data, len); } else { @@ -111,28 +134,7 @@ void UARTComponent::write_array(const uint8_t *data, size_t len) { ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); } } -void UARTComponent::write_str(const char *str) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(str); - } else { - const auto *data = reinterpret_cast(str); - for (size_t i = 0; data[i] != 0; i++) - this->sw_serial_->write_byte(data[i]); - } - ESP_LOGVV(TAG, " Wrote \"%s\"", str); -} -bool UARTComponent::read_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - if (this->hw_serial_ != nullptr) { - *data = this->hw_serial_->read(); - } else { - *data = this->sw_serial_->read_byte(); - } - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); - return true; -} -bool UARTComponent::peek_byte(uint8_t *data) { +bool ESP8266UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; if (this->hw_serial_ != nullptr) { @@ -142,7 +144,7 @@ bool UARTComponent::peek_byte(uint8_t *data) { } return true; } -bool UARTComponent::read_array(uint8_t *data, size_t len) { +bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; if (this->hw_serial_ != nullptr) { @@ -157,28 +159,14 @@ bool UARTComponent::read_array(uint8_t *data, size_t len) { return true; } -bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= int(len)) - return true; - - uint32_t start_time = millis(); - while (this->available() < int(len)) { - if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); - return false; - } - yield(); - } - return true; -} -int UARTComponent::available() { +int ESP8266UartComponent::available() { if (this->hw_serial_ != nullptr) { return this->hw_serial_->available(); } else { return this->sw_serial_->available(); } } -void UARTComponent::flush() { +void ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing..."); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); @@ -186,32 +174,31 @@ void UARTComponent::flush() { this->sw_serial_->flush(); } } -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, - uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size) { +void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, + uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, + size_t rx_buffer_size) { this->bit_time_ = F_CPU / baud_rate; this->rx_buffer_size_ = rx_buffer_size; this->stop_bits_ = stop_bits; this->data_bits_ = data_bits; this->parity_ = parity; - if (tx_pin != -1) { - auto pin = GPIOPin(tx_pin, OUTPUT); - this->gpio_tx_pin_ = &pin; - pin.setup(); - this->tx_pin_ = pin.to_isr(); - this->tx_pin_->digital_write(true); + if (tx_pin != nullptr) { + gpio_tx_pin_ = tx_pin; + gpio_tx_pin_->setup(); + tx_pin_ = gpio_tx_pin_->to_isr(); + tx_pin_.digital_write(true); } - if (rx_pin != -1) { - auto pin = GPIOPin(rx_pin, INPUT); - pin.setup(); - this->gpio_rx_pin_ = &pin; - this->rx_pin_ = pin.to_isr(); - this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; - pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); + if (rx_pin != nullptr) { + gpio_rx_pin_ = rx_pin; + gpio_rx_pin_->setup(); + rx_pin_ = gpio_rx_pin_->to_isr(); + rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT + gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); } } -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { +void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; - const uint32_t start = ESP.getCycleCount(); + const uint32_t start = arch_get_cpu_cycle_count(); uint8_t rec = 0; // Manually unroll the loop for (int i = 0; i < arg->data_bits_; i++) @@ -232,26 +219,26 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; // Clear RX pin so that the interrupt doesn't re-trigger right away again. - arg->rx_pin_->clear_interrupt(); + arg->rx_pin_.clear_interrupt(); } -void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { - if (this->tx_pin_ == nullptr) { +void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { + if (this->gpio_tx_pin_ == nullptr) { ESP_LOGE(TAG, "UART doesn't have TX pins set!"); return; } bool parity_bit = false; bool need_parity_bit = true; if (this->parity_ == UART_CONFIG_PARITY_EVEN) - parity_bit = true; - else if (this->parity_ == UART_CONFIG_PARITY_ODD) parity_bit = false; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + parity_bit = true; else need_parity_bit = false; { InterruptLock lock; uint32_t wait = this->bit_time_; - const uint32_t start = ESP.getCycleCount(); + const uint32_t start = arch_get_cpu_cycle_count(); // Start bit this->write_bit_(false, &wait, start); for (int i = 0; i < this->data_bits_; i++) { @@ -268,17 +255,17 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->wait_(&wait, start); } } -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { - while (ESP.getCycleCount() - start < *wait) +void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { + while (arch_get_cpu_cycle_count() - start < *wait) ; *wait += this->bit_time_; } -bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { +bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { this->wait_(wait, start); - return this->rx_pin_->digital_read(); + return this->rx_pin_.digital_read(); } -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { - this->tx_pin_->digital_write(bit); +void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { + this->tx_pin_.digital_write(bit); this->wait_(wait, start); } uint8_t ESP8266SoftwareSerial::read_byte() { @@ -305,4 +292,4 @@ int ESP8266SoftwareSerial::available() { } // namespace uart } // namespace esphome -#endif // ARDUINO_ARCH_ESP8266 +#endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h new file mode 100644 index 0000000000..eed14f3265 --- /dev/null +++ b/esphome/components/uart/uart_component_esp8266.h @@ -0,0 +1,79 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include +#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 ESP8266SoftwareSerial { + public: + void setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, + uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size); + + uint8_t read_byte(); + uint8_t peek_byte(); + + void flush(); + + void write_byte(uint8_t data); + + int available(); + + protected: + static void gpio_intr(ESP8266SoftwareSerial *arg); + + void wait_(uint32_t *wait, const uint32_t &start); + bool read_bit_(uint32_t *wait, const uint32_t &start); + void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); + + uint32_t bit_time_{0}; + uint8_t *rx_buffer_{nullptr}; + size_t rx_buffer_size_; + volatile size_t rx_in_pos_{0}; + size_t rx_out_pos_{0}; + uint8_t stop_bits_; + uint8_t data_bits_; + UARTParityOptions parity_; + InternalGPIOPin *gpio_tx_pin_{nullptr}; + ISRInternalGPIOPin tx_pin_; + InternalGPIOPin *gpio_rx_pin_{nullptr}; + ISRInternalGPIOPin rx_pin_; +}; + +class ESP8266UartComponent : 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; + + uint32_t get_config(); + + protected: + void check_logger_conflict() override; + + HardwareSerial *hw_serial_{nullptr}; + ESP8266SoftwareSerial *sw_serial_{nullptr}; + + private: + static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp new file mode 100644 index 0000000000..1cccd5821e --- /dev/null +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -0,0 +1,201 @@ +#ifdef USE_ESP_IDF + +#include "uart_component_esp_idf.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +namespace esphome { +namespace uart { +static const char *const TAG = "uart.idf"; + +uart_config_t IDFUARTComponent::get_config_() { + uart_parity_t parity = UART_PARITY_DISABLE; + if (this->parity_ == UART_CONFIG_PARITY_EVEN) + parity = UART_PARITY_EVEN; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + parity = UART_PARITY_ODD; + + uart_word_length_t data_bits; + switch (this->data_bits_) { + case 5: + data_bits = UART_DATA_5_BITS; + break; + case 6: + data_bits = UART_DATA_6_BITS; + break; + case 7: + data_bits = UART_DATA_7_BITS; + break; + case 8: + data_bits = UART_DATA_8_BITS; + break; + default: + data_bits = UART_DATA_BITS_MAX; + break; + } + + uart_config_t uart_config; + uart_config.baud_rate = this->baud_rate_; + uart_config.data_bits = data_bits; + 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; + uart_config.source_clk = UART_SCLK_APB; + uart_config.rx_flow_ctrl_thresh = 122; + + return uart_config; +} + +void IDFUARTComponent::setup() { + static uint8_t next_uart_num = 0; +#ifdef USE_LOGGER + if (logger::global_logger->get_uart_num() == next_uart_num) + next_uart_num++; +#endif + 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++; + ESP_LOGCONFIG(TAG, "Setting up UART %u...", this->uart_num_); + + this->lock_ = xSemaphoreCreateMutex(); + + xSemaphoreTake(this->lock_, portMAX_DELAY); + + 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; + } + + err = uart_driver_install(this->uart_num_, this->rx_buffer_size_, 0, 0, nullptr, 0); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + this->mark_failed(); + 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::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + ESP_LOGCONFIG(TAG, " Number: %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, " 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 IDFUARTComponent::write_array(const uint8_t *data, size_t len) { + xSemaphoreTake(this->lock_, portMAX_DELAY); + uart_write_bytes(this->uart_num_, data, len); + xSemaphoreGive(this->lock_); + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } +} +bool IDFUARTComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + xSemaphoreTake(this->lock_, portMAX_DELAY); + if (this->has_peek_) + *data = this->peek_byte_; + else { + int len = uart_read_bytes(this->uart_num_, data, 1, 20 / portTICK_RATE_MS); + if (len == 0) { + *data = 0; + } else { + this->has_peek_ = true; + this->peek_byte_ = *data; + } + } + xSemaphoreGive(this->lock_); + return true; +} +bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { + size_t length_to_read = len; + if (!this->check_read_timeout_(len)) + return false; + xSemaphoreTake(this->lock_, portMAX_DELAY); + if (this->has_peek_) { + length_to_read--; + *data = this->peek_byte_; + data++; + this->has_peek_ = false; + } + if (length_to_read > 0) + uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_RATE_MS); + + xSemaphoreGive(this->lock_); + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } + + return true; +} + +int IDFUARTComponent::available() { + size_t available; + + xSemaphoreTake(this->lock_, portMAX_DELAY); + uart_get_buffered_data_len(this->uart_num_, &available); + if (this->has_peek_) + available++; + xSemaphoreGive(this->lock_); + + return available; +} + +void IDFUARTComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + xSemaphoreTake(this->lock_, portMAX_DELAY); + uart_wait_tx_done(this->uart_num_, portMAX_DELAY); + xSemaphoreGive(this->lock_); +} + +void IDFUARTComponent::check_logger_conflict() {} + +} // namespace uart +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h new file mode 100644 index 0000000000..27fb80d2cc --- /dev/null +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include +#include "esphome/core/component.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +class IDFUARTComponent : 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; + + protected: + void check_logger_conflict() override; + uart_port_t uart_num_; + uart_config_t get_config_(); + SemaphoreHandle_t lock_; + + bool has_peek_{false}; + uint8_t peek_byte_; +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/uln2003/uln2003.h b/esphome/components/uln2003/uln2003.h index 4bcf1e88e3..4f559ed9a0 100644 --- a/esphome/components/uln2003/uln2003.h +++ b/esphome/components/uln2003/uln2003.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/stepper/stepper.h" namespace esphome { diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 77b08b3324..f7026e884c 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_TRIGGER_PIN, CONF_TIMEOUT, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -22,11 +21,10 @@ UltrasonicSensorComponent = ultrasonic_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -37,13 +35,6 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, - cv.Optional("timeout_meter"): cv.invalid( - "The timeout_meter option has been renamed " "to 'timeout' in 1.12." - ), - cv.Optional("timeout_time"): cv.invalid( - "The timeout_time option has been removed. Please " - "use 'timeout' in 1.12." - ), } ) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index e53cd7cf7a..9f47f9f6b9 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -1,5 +1,6 @@ #include "ultrasonic_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" namespace esphome { namespace ultrasonic { @@ -11,22 +12,31 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->setup(); this->trigger_pin_->digital_write(false); this->echo_pin_->setup(); + // isr is faster to access + echo_isr_ = echo_pin_->to_isr(); } void UltrasonicSensorComponent::update() { this->trigger_pin_->digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_->digital_write(false); - uint32_t time = pulseIn( // NOLINT - this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); + const uint32_t start = micros(); + while (micros() - start < timeout_us_ && echo_isr_.digital_read()) + ; + while (micros() - start < timeout_us_ && !echo_isr_.digital_read()) + ; + const uint32_t pulse_start = micros(); + while (micros() - start < timeout_us_ && echo_isr_.digital_read()) + ; + const uint32_t pulse_end = micros(); - ESP_LOGV(TAG, "Echo took %uµs", time); + ESP_LOGV(TAG, "Echo took %uµs", pulse_end - pulse_start); - if (time == 0) { + 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(time); + float result = UltrasonicSensorComponent::us_to_m(pulse_end - pulse_start); ESP_LOGD(TAG, "'%s' - Got distance: %.2f m", this->name_.c_str(), result); this->publish_state(result); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 633c1b17fb..e0d71b99ef 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/gpio.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -10,7 +10,7 @@ namespace ultrasonic { class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent { public: void set_trigger_pin(GPIOPin *trigger_pin) { trigger_pin_ = trigger_pin; } - void set_echo_pin(GPIOPin *echo_pin) { echo_pin_ = echo_pin; } + void set_echo_pin(InternalGPIOPin *echo_pin) { echo_pin_ = echo_pin; } /// Set the timeout for waiting for the echo in µs. void set_timeout_us(uint32_t timeout_us); @@ -34,7 +34,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent /// Helper function to convert the specified distance in meters to the echo duration in µs. GPIOPin *trigger_pin_; - GPIOPin *echo_pin_; + InternalGPIOPin *echo_pin_; + ISRInternalGPIOPin echo_isr_; uint32_t timeout_us_{}; /// 2 meters. uint32_t pulse_time_us_{}; }; diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index eaaee5a2d5..7989f3befc 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,8 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, ) @@ -14,7 +13,10 @@ UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingCompone CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp index 755795ad53..40325d2a36 100644 --- a/esphome/components/uptime/uptime_sensor.cpp +++ b/esphome/components/uptime/uptime_sensor.cpp @@ -1,6 +1,7 @@ #include "uptime_sensor.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace uptime { diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt index fe33583414..f7a234d023 100644 --- a/esphome/components/vl53l0x/LICENSE.txt +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -3,7 +3,7 @@ by Pololu (Pololu Corporation), which in turn is based on the VL53L0X API from ST. The code has been adapted to work with ESPHome's i2c APIs. Please see the top-level LICENSE.txt for information about ESPHome's license. The licenses for Pololu's and ST's software are included below. -Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). +Originally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). ================================================================= diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 8a9667a1bd..0ce3197366 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -42,11 +41,10 @@ def check_timeout(value): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 65a4ec72bb..d68d69b79c 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -36,8 +36,6 @@ void VL53L0XSensor::setup() { if (!esphome::vl53l0x::VL53L0XSensor::enable_pin_setup_complete) { for (auto &vl53_sensor : vl53_sensors) { if (vl53_sensor->enable_pin_ != nullptr) { - // Disable the enable pin to force vl53 to HW Standby mode - ESP_LOGD(TAG, "i2c vl53l0x disable enable pins: GPIO%u", (vl53_sensor->enable_pin_)->get_pin()); // Set enable pin as OUTPUT and disable the enable pin to force vl53 to HW Standby mode vl53_sensor->enable_pin_->setup(); vl53_sensor->enable_pin_->digital_write(false); @@ -111,7 +109,7 @@ void VL53L0XSensor::setup() { reg(0xFF) = 0x00; reg(0x80) = 0x00; - uint8_t ref_spad_map[6]; + uint8_t ref_spad_map[6] = {}; this->read_bytes(0xB0, ref_spad_map, 6); reg(0xFF) = 0x01; @@ -294,7 +292,7 @@ void VL53L0XSensor::loop() { } if (this->waiting_for_interrupt_) { if (reg(0x13).get() & 0x07) { - uint16_t range_mm; + uint16_t range_mm = 0; this->read_byte_16(0x14 + 10, &range_mm); reg(0x0B) = 0x01; this->waiting_for_interrupt_ = false; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 0c37df67a2..a2e24e7550 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -3,6 +3,7 @@ #include #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 3e1132bb1d..64f5597a65 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -31,15 +31,24 @@ WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) +WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper4P2InBV2", WaveshareEPaper +) WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) WaveshareEPaper7P5In = waveshare_epaper_ns.class_( "WaveshareEPaper7P5In", WaveshareEPaper ) +WaveshareEPaper7P5InBC = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InBC", WaveshareEPaper +) WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) +WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InDKE", WaveshareEPaper +) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel") @@ -51,21 +60,25 @@ MODELS = { "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), "2.70in": ("b", WaveshareEPaper2P7In), "2.90in-b": ("b", WaveshareEPaper2P9InB), "4.20in": ("b", WaveshareEPaper4P2In), + "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), "7.50in": ("b", WaveshareEPaper7P5In), + "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), + "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), } def validate_full_update_every_only_type_a(value): if CONF_FULL_UPDATE_EVERY not in value: return value - if MODELS[value[CONF_MODEL]][0] != "a": + if MODELS[value[CONF_MODEL]][0] == "b": raise cv.Invalid( "The 'full_update_every' option is only available for models " "'1.54in', '1.54inV2', '2.13in', '2.90in', and '2.90inV2'." @@ -96,7 +109,7 @@ async def to_code(config): if model_type == "a": rhs = WaveshareEPaperTypeA.new(model) var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA) - elif model_type == "b": + elif model_type in ("b", "c"): rhs = model.new() var = cg.Pvariable(config[CONF_ID], rhs, model) else: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 4518dd60df..92fa289cfa 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -163,6 +163,17 @@ void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== 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_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + } + // COMMAND DRIVER OUTPUT CONTROL this->command(0x01); this->data(this->get_height_internal() - 1); @@ -193,6 +204,7 @@ void WaveshareEPaperTypeA::initialize() { case TTGO_EPAPER_2_13_IN_B1: this->data(0x01); // x increase, y decrease : as in demo code break; + case TTGO_EPAPER_2_13_IN_B74: case WAVESHARE_EPAPER_2_9_IN_V2: this->data(0x03); // from top left to bottom right // RAM content option for Display Update @@ -222,6 +234,9 @@ void WaveshareEPaperTypeA::dump_config() { case TTGO_EPAPER_2_13_IN_B73: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B73)"); break; + case TTGO_EPAPER_2_13_IN_B74: + ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B74)"); + break; case TTGO_EPAPER_2_13_IN_B1: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B1)"); break; @@ -256,6 +271,9 @@ void HOT WaveshareEPaperTypeA::display() { case TTGO_EPAPER_2_13_IN_B73: this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B73 : PARTIAL_UPDATE_LUT_TTGO_B73, LUT_SIZE_TTGO_B73); break; + case TTGO_EPAPER_2_13_IN_B74: + // there is no LUT + break; case TTGO_EPAPER_2_13_IN_B1: this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B1 : PARTIAL_UPDATE_LUT_TTGO_B1, LUT_SIZE_TTGO_B1); break; @@ -289,7 +307,12 @@ 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); @@ -341,6 +364,9 @@ void HOT WaveshareEPaperTypeA::display() { this->data(full_update ? 0xF7 : 0xFF); } else if (this->model_ == TTGO_EPAPER_2_13_IN_B73) { this->data(0xC7); + } else if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { + // this->data(0xC7); + this->data(full_update ? 0xF7 : 0xFF); } else { this->data(0xC4); } @@ -363,6 +389,7 @@ int WaveshareEPaperTypeA::get_width_internal() { case TTGO_EPAPER_2_13_IN: return 128; case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: return 128; case TTGO_EPAPER_2_13_IN_B1: return 128; @@ -384,6 +411,7 @@ int WaveshareEPaperTypeA::get_height_internal() { case TTGO_EPAPER_2_13_IN: return 250; case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: return 250; case TTGO_EPAPER_2_13_IN_B1: return 250; @@ -598,7 +626,7 @@ void WaveshareEPaper2P9InB::initialize() { this->data(0x9F); // COMMAND RESOLUTION SETTING - // set to 128x296 by COMMAND PANNEL SETTING + // set to 128x296 by COMMAND PANEL SETTING // COMMAND VCOM AND DATA INTERVAL SETTING // use defaults for white border and ESPHome image polarity @@ -765,6 +793,62 @@ void WaveshareEPaper4P2In::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 4.20in Type B (LUT from OTP) +// Datasheet: +// - https://www.waveshare.com/w/upload/2/20/4.2inch-e-paper-module-user-manual-en.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_4in2b_V2.c +// ======================================================== +void WaveshareEPaper4P2InBV2::initialize() { + // these exact timings are required for a proper reset/init + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x0f); // LUT from OTP +} + +void HOT WaveshareEPaper4P2InBV2::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + this->start_data_(); + for (int i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0xFF); + this->end_data_(); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper4P2InBV2::get_width_internal() { return 400; } +int WaveshareEPaper4P2InBV2::get_height_internal() { return 300; } +void WaveshareEPaper4P2InBV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 4.2in (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); +} + void WaveshareEPaper5P8In::initialize() { // COMMAND POWER SETTING this->command(0x01); @@ -996,5 +1080,232 @@ void WaveshareEPaper7P5InV2::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +/* 7.50in-bc */ +void WaveshareEPaper7P5InBC::initialize() { + /* The command sequence is similar to the 7P5In display but differs in subtle ways + to allow for faster updates. */ + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x37); + this->data(0x00); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0xCF); + this->data(0x08); + + // COMMAND PLL CONTROL + this->command(0x30); + this->data(0x3A); + + // COMMAND VCM_DC_SETTING: all temperature range + this->command(0x82); + this->data(0x28); + + // COMMAND BOOSTER SOFT START + this->command(0x06); + this->data(0xC7); + this->data(0xCC); + this->data(0x15); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x77); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + + // COMMAND FLASH CONTROL + this->command(0x65); + this->data(0x00); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x02); // 640 >> 8 + this->data(0x80); + this->data(0x01); // 384 >> 8 + this->data(0x80); + + // COMMAND FLASH MODE + this->command(0xE5); + this->data(0x03); +} + +void HOT WaveshareEPaper7P5InBC::display() { + // COMMAND DATA START TRANSMISSION 1 + this->command(0x10); + this->start_data_(); + + for (size_t i = 0; i < this->get_buffer_length_(); i++) { + // A line of eight source pixels (each a bit in this byte) + uint8_t eight_pixels = this->buffer_[i]; + + for (uint8_t j = 0; j < 8; j += 2) { + /* For bichromatic displays, each byte represents two pixels. Each nibble encodes a pixel: 0=white, 3=black, + 4=color. Therefore, e.g. 0x44 = two adjacent color pixels, 0x33 is two adjacent black pixels, etc. If you want + to draw using the color pixels, change '0x30' with '0x40' and '0x03' with '0x04' below. */ + uint8_t left_nibble = (eight_pixels & 0x80) ? 0x30 : 0x00; + eight_pixels <<= 1; + uint8_t right_nibble = (eight_pixels & 0x80) ? 0x03 : 0x00; + eight_pixels <<= 1; + this->write_byte(left_nibble | right_nibble); + } + App.feed_wdt(); + } + this->end_data_(); + + // Unlike the 7P5In display, we send the "power on" command here rather than during initialization + // COMMAND POWER ON + this->command(0x04); + + // COMMAND DISPLAY REFRESH + this->command(0x12); +} + +int WaveshareEPaper7P5InBC::get_width_internal() { return 640; } + +int WaveshareEPaper7P5InBC::get_height_internal() { return 384; } + +void WaveshareEPaper7P5InBC::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5in-bc"); + 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); +} + +static const uint8_t LUT_SIZE_TTGO_DKE_PART = 153; + +static const uint8_t PART_UPDATE_LUT_TTGO_DKE[LUT_SIZE_TTGO_DKE_PART] = { + 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, 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, 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, + // 0x22, 0x17, 0x41, 0x0, 0x32, 0x32 +}; + +void WaveshareEPaper2P13InDKE::initialize() {} +void HOT WaveshareEPaper2P13InDKE::display() { + bool partial = this->at_update_ != 0; + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + + if (partial) + ESP_LOGI(TAG, "Performing partial e-paper update."); + else + ESP_LOGI(TAG, "Performing full e-paper update."); + + // start and set up data format + this->command(0x12); + this->wait_until_idle_(); + + this->command(0x11); + this->data(0x03); + this->command(0x44); + this->data(1); + this->data(this->get_width_internal() / 8); + this->command(0x45); + this->data(0); + this->data(0); + this->data(this->get_height_internal()); + this->data(0); + this->command(0x4e); + this->data(1); + this->command(0x4f); + this->data(0); + this->data(0); + + if (!partial) { + // send data + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // commit + this->command(0x20); + this->wait_until_idle_(); + } else { + // set up partial update + this->command(0x32); + for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE) + this->data(v); + this->command(0x3F); + this->data(0x22); + + this->command(0x03); + this->data(0x17); + this->command(0x04); + this->data(0x41); + this->data(0x00); + this->data(0x32); + this->command(0x2C); + this->data(0x32); + + 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); + this->wait_until_idle_(); + + // send data + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // commit as partial + this->command(0x22); + this->data(0xCF); + this->command(0x20); + 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."); +} + +int WaveshareEPaper2P13InDKE::get_width_internal() { return 128; } +int WaveshareEPaper2P13InDKE::get_height_internal() { return 250; } +int WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } +void WaveshareEPaper2P13InDKE::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + 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 WaveshareEPaper2P13InDKE::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.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 8ab77d653b..b50596643d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -73,6 +73,7 @@ enum WaveshareEPaperTypeAModel { TTGO_EPAPER_2_13_IN, TTGO_EPAPER_2_13_IN_B73, TTGO_EPAPER_2_13_IN_B1, + TTGO_EPAPER_2_13_IN_B74, }; class WaveshareEPaperTypeA : public WaveshareEPaper { @@ -115,6 +116,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, WAVESHARE_EPAPER_4_2_IN, + WAVESHARE_EPAPER_4_2_IN_B_V2, WAVESHARE_EPAPER_7_5_IN, WAVESHARE_EPAPER_7_5_INV2, }; @@ -202,6 +204,34 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper4P2InBV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0xF7); // border floating + + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check code + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper5P8In : public WaveshareEPaper { public: void initialize() override; @@ -248,6 +278,29 @@ class WaveshareEPaper7P5In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InBC : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper7P5InV2 : public WaveshareEPaper { public: void initialize() override; @@ -271,5 +324,33 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P13InDKE : public WaveshareEPaper { + public: + void initialize() override; + + 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); + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + int idle_timeout_() override; + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; +}; + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a181f83c64..240ba7c8a0 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_USERNAME, CONF_PASSWORD, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority AUTO_LOAD = ["json", "web_server_base"] @@ -34,8 +34,8 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_JS_INCLUDE): cv.file_, cv.Optional(CONF_AUTH): cv.Schema( { - cv.Required(CONF_USERNAME): cv.string_strict, - cv.Required(CONF_PASSWORD): cv.string_strict, + cv.Required(CONF_USERNAME): cv.All(cv.string_strict, cv.Length(min=1)), + cv.Required(CONF_PASSWORD): cv.All(cv.string_strict, cv.Length(min=1)), } ), cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( @@ -57,13 +57,15 @@ async def to_code(config): cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) if CONF_AUTH in config: - cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME])) - cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) + 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("WEBSERVER_CSS_INCLUDE") - with open(config[CONF_CSS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("WEBSERVER_JS_INCLUDE") - with open(config[CONF_JS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7a6d877d8f..e99431be36 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,13 +1,21 @@ +#ifdef USE_ARDUINO + #include "web_server.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/entity_base.h" #include "esphome/core/util.h" #include "esphome/components/json/json_util.h" +#include "esphome/components/network/util.h" #include "StreamString.h" #include +#ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" +#endif + #ifdef USE_LOGGER #include #endif @@ -21,7 +29,8 @@ namespace web_server { static const char *const TAG = "web_server"; -void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &klass, const std::string &action) { +void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, + const std::function &action_func = nullptr) { if (obj->is_internal()) return; stream->print("print(obj->get_name().c_str()); stream->print(""); stream->print(action.c_str()); + if (action_func) { + action_func(*stream, obj); + } stream->print(""); stream->print(""); } @@ -119,6 +131,18 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->cover_json(obj).c_str(), "state"); #endif + +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + if (!obj->is_internal()) + client->send(this->number_json(obj, obj->state).c_str(), "state"); +#endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + if (!obj->is_internal()) + client->send(this->select_json(obj, obj->state).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -134,17 +158,16 @@ void WebServer::setup() { } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); - ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->base_->get_port()); - if (this->using_auth()) { - ESP_LOGCONFIG(TAG, " Basic authentication enabled"); - } + ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; - stream->print(F("")); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8>" + "<meta name=\"viewport\" content=\"width=device-width, " + "initial-scale=1.0\"><title>")); stream->print(title.c_str()); stream->print(F("")); #ifdef WEBSERVER_CSS_INCLUDE @@ -196,6 +219,26 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "cover", ""); #endif +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + write_row(stream, obj, "number", ""); +#endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { + select::Select *select = (select::Select *) obj; + stream.print(""); + }); +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { - case fan::FAN_SPEED_LOW: + case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "low"; break; - case fan::FAN_SPEED_MEDIUM: + case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "medium"; break; - case fan::FAN_SPEED_HIGH: + case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "high"; break; } @@ -404,7 +448,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); - call.set_speed(speed.c_str()); + call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); @@ -517,7 +561,7 @@ std::string WebServer::light_json(light::LightState *obj) { return json::build_json([obj](JsonObject &root) { root["id"] = "light-" + obj->get_object_id(); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; - obj->dump_json(root); + light::LightJSONSchema::dump_json(*obj, root); }); } #endif @@ -584,6 +628,77 @@ std::string WebServer::cover_json(cover::Cover *obj) { } #endif +#ifdef USE_NUMBER +void WebServer::on_number_update(number::Number *obj, float state) { + this->events_.send(this->number_json(obj, state).c_str(), "state"); +} +void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_numbers()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->number_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::number_json(number::Number *obj, float value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "number-" + obj->get_object_id(); + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + root["state"] = buffer; + root["value"] = value; + }); +} +#endif + +#ifdef USE_SELECT +void WebServer::on_select_update(select::Select *obj, const std::string &state) { + this->events_.send(this->select_json(obj, state).c_str(), "state"); +} +void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_selects()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->select_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (request->hasParam("option")) { + String option = request->getParam("option")->value(); + call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::select_json(select::Select *obj, const std::string &value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "select-" + obj->get_object_id(); + root["state"] = value; + root["value"] = value; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -636,13 +751,19 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_NUMBER + if (request->method() == HTTP_GET && match.domain == "number") + return true; +#endif + +#ifdef USE_SELECT + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { - if (this->using_auth() && !request->authenticate(this->username_, this->password_)) { - return request->requestAuthentication(); - } - if (request->url() == "/") { this->handle_index_request(request); return; @@ -711,9 +832,25 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_NUMBER + if (match.domain == "number") { + this->handle_number_request(request, match); + return; + } +#endif + +#ifdef USE_SELECT + if (match.domain == "select") { + this->handle_select_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } } // namespace web_server } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 89e23b7071..021d5a0646 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/components/web_server_base/web_server_base.h" @@ -30,10 +32,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { public: WebServer(web_server_base::WebServerBase *base) : base_(base) {} - void set_username(const char *username) { username_ = username; } - - void set_password(const char *password) { password_ = password; } - /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css * @@ -83,8 +81,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_js_request(AsyncWebServerRequest *request); #endif - bool using_auth() { return username_ != nullptr && password_ != nullptr; } - #ifdef USE_SENSOR void on_sensor_update(sensor::Sensor *obj, float state) override; /// Handle a sensor request under '/sensor/'. @@ -154,6 +150,24 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string cover_json(cover::Cover *obj); #endif +#ifdef USE_NUMBER + void on_number_update(number::Number *obj, float state) override; + /// Handle a number request under '/number/'. + void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the number state with its value as a JSON string. + std::string number_json(number::Number *obj, float value); +#endif + +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; + /// Handle a select request under '/select/'. + void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the number state with its value as a JSON string. + std::string select_json(select::Select *obj, const std::string &value); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. @@ -164,8 +178,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { protected: web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; - const char *username_{nullptr}; - const char *password_{nullptr}; const char *css_url_{nullptr}; const char *css_include_{nullptr}; const char *js_url_{nullptr}; @@ -174,3 +186,5 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { } // namespace web_server } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 37f3c989e4..95d59a863e 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -24,6 +24,8 @@ async def to_code(config): await cg.register_component(var, config) if CORE.is_esp32: + cg.add_library("WiFi", None) cg.add_library("FS", None) - # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library("ESPAsyncWebServer-esphome", "1.2.7") + cg.add_library("Update", None) + # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json + cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.0") diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 85711704b9..3c269b28b8 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,12 +1,14 @@ +#ifdef USE_ARDUINO + #include "web_server_base.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #endif @@ -15,6 +17,17 @@ namespace web_server_base { static const char *const TAG = "web_server_base"; +void WebServerBase::add_handler(AsyncWebHandler *handler) { + // remove all handlers + + if (!credentials_.username.empty()) { + handler = new internal::AuthMiddlewareHandler(handler, &credentials_); + } + this->handlers_.push_back(handler); + if (this->server_ != nullptr) + this->server_->addHandler(handler); +} + void report_ota_error() { StreamString ss; Update.printError(ss); @@ -27,11 +40,12 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin if (index == 0) { ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); this->ota_read_length_ = 0; -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 Update.runAsync(true); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); #endif -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 if (Update.isRunning()) Update.abort(); success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); @@ -86,7 +100,9 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { request->send(response); } -void WebServerBase::add_ota_handler() { this->add_handler(new OTARequestHandler(this)); } +void WebServerBase::add_ota_handler() { + this->add_handler(new OTARequestHandler(this)); // NOLINT +} float WebServerBase::get_setup_priority() const { // Before WiFi (captive portal) return setup_priority::WIFI + 2.0f; @@ -94,3 +110,5 @@ float WebServerBase::get_setup_priority() const { } // namespace web_server_base } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index b6024ceafa..bc37337ca5 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,5 +1,9 @@ #pragma once +#ifdef USE_ARDUINO + +#include +#include #include "esphome/core/component.h" #include @@ -7,6 +11,68 @@ namespace esphome { namespace web_server_base { +namespace internal { + +class MiddlewareHandler : public AsyncWebHandler { + public: + MiddlewareHandler(AsyncWebHandler *next) : next_(next) {} + + bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); } + void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override { + next_->handleUpload(request, filename, index, data, len, final); + } + void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { + next_->handleBody(request, data, len, index, total); + } + bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } + + protected: + AsyncWebHandler *next_; +}; + +struct Credentials { + std::string username; + std::string password; +}; + +class AuthMiddlewareHandler : public MiddlewareHandler { + public: + AuthMiddlewareHandler(AsyncWebHandler *next, Credentials *credentials) + : MiddlewareHandler(next), credentials_(credentials) {} + + bool check_auth(AsyncWebServerRequest *request) { + bool success = request->authenticate(credentials_->username.c_str(), credentials_->password.c_str()); + if (!success) { + request->requestAuthentication(); + } + return success; + } + + void handleRequest(AsyncWebServerRequest *request) override { + if (!check_auth(request)) + return; + MiddlewareHandler::handleRequest(request); + } + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override { + if (!check_auth(request)) + return; + MiddlewareHandler::handleUpload(request, filename, index, data, len, final); + } + void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { + if (!check_auth(request)) + return; + MiddlewareHandler::handleBody(request, data, len, index, total); + } + + protected: + Credentials *credentials_; +}; + +} // namespace internal + class WebServerBase : public Component { public: void init() { @@ -14,7 +80,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = new AsyncWebServer(this->port_); + this->server_ = std::make_shared(this->port_); this->server_->begin(); for (auto *handler : this->handlers_) @@ -25,20 +91,16 @@ class WebServerBase : public Component { void deinit() { this->initialized_--; if (this->initialized_ == 0) { - delete this->server_; this->server_ = nullptr; } } - AsyncWebServer *get_server() const { return server_; } + std::shared_ptr get_server() const { return server_; } float get_setup_priority() const override; - void add_handler(AsyncWebHandler *handler) { - // remove all handlers + void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } + void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); } - this->handlers_.push_back(handler); - if (this->server_ != nullptr) - this->server_->addHandler(handler); - } + void add_handler(AsyncWebHandler *handler); void add_ota_handler(); @@ -50,8 +112,9 @@ class WebServerBase : public Component { int initialized_{0}; uint16_t port_{80}; - AsyncWebServer *server_{nullptr}; + std::shared_ptr server_{nullptr}; std::vector handlers_; + internal::Credentials credentials_; }; class OTARequestHandler : public AsyncWebHandler { @@ -74,3 +137,5 @@ class OTARequestHandler : public AsyncWebHandler { } // namespace web_server_base } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 07296b6fa5..d705b42a8c 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -48,7 +48,7 @@ void WhirlpoolClimate::transmit_state() { this->powered_on_assumed = powered_on; } switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: // set fan auto // set temp auto temp // set sleep false @@ -239,7 +239,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; case WHIRLPOOL_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c45e179bc4..19e4046711 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,8 +1,8 @@ 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 -from esphome.components.network import add_mdns_library from esphome.const import ( CONF_AP, CONF_BSSID, @@ -23,7 +23,6 @@ from esphome.const import ( CONF_STATIC_IP, CONF_SUBNET, CONF_USE_ADDRESS, - CONF_ENABLE_MDNS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, @@ -33,6 +32,7 @@ from esphome.const import ( CONF_EAP, ) from esphome.core import CORE, HexInt, coroutine_with_priority +from esphome.components.network import IPAddress from . import wpa2_eap @@ -40,7 +40,6 @@ AUTO_LOAD = ["network"] wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") -IPAddress = cg.global_ns.class_("IPAddress") ManualIP = wifi_ns.struct("ManualIP") WiFiComponent = wifi_ns.class_("WiFiComponent", cg.Component) WiFiAP = wifi_ns.struct("WiFiAP") @@ -137,6 +136,52 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( ) +def final_validate(config): + has_sta = bool(config.get(CONF_NETWORKS, True)) + has_ap = CONF_AP in config + has_improv = "esp32_improv" in fv.full_config.get() + if (not has_sta) and (not has_ap) and (not has_improv): + raise cv.Invalid( + "Please specify at least an SSID or an Access Point to create." + ) + + +def final_validate_power_esp32_ble(value): + if not CORE.is_esp32: + return + if value != "NONE": + # WiFi should be in modem sleep (!=NONE) with BLE coexistence + # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-guides/wifi.html#station-sleep + return + for conflicting in [ + "esp32_ble", + "esp32_ble_beacon", + "esp32_ble_server", + "esp32_ble_tracker", + ]: + try: + cv.require_framework_version(esp32_arduino=cv.Version(1, 0, 5))(None) + except cv.Invalid: + pass + else: + raise cv.Invalid( + f"power_save_mode NONE is incompatible with {conflicting}. " + f"Please remove the power save mode. See also " + f"https://github.com/esphome/issues/issues/2141#issuecomment-865688582" + ) + + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_POWER_SAVE_MODE): final_validate_power_esp32_ble, + }, + extra=cv.ALLOW_EXTRA, + ), + final_validate, +) + + def _validate(config): if CONF_PASSWORD in config and CONF_SSID not in config: raise cv.Invalid("Cannot have WiFi password without SSID!") @@ -157,9 +202,8 @@ def _validate(config): config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) if (CONF_NETWORKS not in config) and (CONF_AP not in config): - raise cv.Invalid( - "Please specify at least an SSID or an Access Point " "to create." - ) + config = config.copy() + config[CONF_NETWORKS] = [] if config.get(CONF_FAST_CONNECT, False): networks = config.get(CONF_NETWORKS, []) @@ -189,7 +233,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, - cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -202,8 +245,9 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=10.0, max=20.5) ), - cv.Optional("hostname"): cv.invalid( - "The hostname option has been removed in 1.11.0" + cv.Optional("enable_mdns"): cv.invalid( + "This option has been removed. Please use the [disabled] option under the " + "new mdns component instead." ), } ), @@ -261,7 +305,7 @@ def wifi_network(config, static_ip): cg.add(ap.set_password(config[CONF_PASSWORD])) if CONF_EAP in config: cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) - cg.add_define("ESPHOME_WIFI_WPA2_EAP") + cg.add_define("USE_WIFI_WPA2_EAP") if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: @@ -298,12 +342,11 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + elif CORE.is_esp32 and CORE.using_arduino: + cg.add_library("WiFi", None) cg.add_define("USE_WIFI") - if config[CONF_ENABLE_MDNS]: - add_mdns_library() - # Register at end for OTA safe mode await cg.register_component(var, config) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5e02307598..703afa99bc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,9 +1,9 @@ #include "wifi_component.h" -#ifdef ARDUINO_ARCH_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP_IDF) #include #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #endif @@ -14,7 +14,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/util.h" #include "esphome/core/application.h" @@ -39,7 +39,7 @@ void WiFiComponent::setup() { this->wifi_pre_setup_(); uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences.make_preference(hash, true); + this->pref_ = global_preferences->make_preference(hash, true); SavedWifiSettings save{}; if (this->pref_.load(&save)) { @@ -83,12 +83,10 @@ void WiFiComponent::setup() { esp32_improv::global_improv_component->start(); #endif this->wifi_apply_hostname_(); -#if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS) - network_setup_mdns(); -#endif } void WiFiComponent::loop() { + this->wifi_loop_(); const uint32_t now = millis(); if (this->has_sta()) { @@ -158,18 +156,16 @@ void WiFiComponent::loop() { } } } - - network_tick_mdns(); } WiFiComponent::WiFiComponent() { global_wifi_component = this; } -bool WiFiComponent::has_ap() const { return !this->ap_.get_ssid().empty(); } +bool WiFiComponent::has_ap() const { return this->has_ap_; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; } -IPAddress WiFiComponent::get_ip_address() { +network::IPAddress WiFiComponent::get_ip_address() { if (this->has_sta()) - return this->wifi_sta_ip_(); + return this->wifi_sta_ip(); if (this->has_ap()) return this->wifi_soft_ap_ip(); return {}; @@ -187,22 +183,31 @@ void WiFiComponent::setup_ap_config_() { if (this->ap_setup_) return; + if (this->ap_.get_ssid().empty()) { + std::string name = App.get_name(); + if (name.length() > 32) { + if (App.is_name_add_mac_suffix_enabled()) { + name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address + } else { + name = name.substr(0, 32); + } + } + this->ap_.set_ssid(name); + } + ESP_LOGCONFIG(TAG, "Setting up AP..."); ESP_LOGCONFIG(TAG, " AP SSID: '%s'", this->ap_.get_ssid().c_str()); ESP_LOGCONFIG(TAG, " AP Password: '%s'", this->ap_.get_password().c_str()); if (this->ap_.get_manual_ip().has_value()) { auto manual = *this->ap_.get_manual_ip(); - ESP_LOGCONFIG(TAG, " AP Static IP: '%s'", manual.static_ip.toString().c_str()); - ESP_LOGCONFIG(TAG, " AP Gateway: '%s'", manual.gateway.toString().c_str()); - ESP_LOGCONFIG(TAG, " AP Subnet: '%s'", manual.subnet.toString().c_str()); + ESP_LOGCONFIG(TAG, " AP Static IP: '%s'", manual.static_ip.str().c_str()); + ESP_LOGCONFIG(TAG, " AP Gateway: '%s'", manual.gateway.str().c_str()); + ESP_LOGCONFIG(TAG, " AP Subnet: '%s'", manual.subnet.str().c_str()); } this->ap_setup_ = this->wifi_start_ap_(this->ap_); - ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str()); -#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) - network_setup_mdns(this->wifi_soft_ap_ip(), 1); -#endif + ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().str().c_str()); if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -212,7 +217,10 @@ 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; } +void WiFiComponent::set_ap(const WiFiAP &ap) { + this->ap_ = ap; + this->has_ap_ = true; +} void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); @@ -224,11 +232,15 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid)); strncpy(save.password, password.c_str(), sizeof(save.password)); this->pref_.save(&save); + // ensure it's written immediately + global_preferences->sync(); WiFiAP sta{}; sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + this->start_scanning(); } void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { @@ -243,7 +255,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " BSSID: Not Set"); } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); EAPAuth eap_config = ap.get_eap().value(); @@ -259,7 +271,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP } #endif if (ap.get_channel().has_value()) { @@ -269,9 +281,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); - ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.toString().c_str(), - m.gateway.toString().c_str(), m.subnet.toString().c_str(), m.dns1.toString().c_str(), - m.dns2.toString().c_str()); + ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), + m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); } else { ESP_LOGV(TAG, " Using DHCP IP"); } @@ -292,7 +303,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { this->action_started_ = millis(); } -void print_signal_bars(int8_t rssi, char *buf) { +const LogString *get_signal_bars(int8_t rssi) { // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -302,62 +313,58 @@ void print_signal_bars(int8_t rssi, char *buf) { // FULL BLOCK // Unicode: U+2588, UTF-8: E2 96 88 if (rssi >= -50) { - sprintf(buf, "\033[0;32m" // green - "\xe2\x96\x82" - "\xe2\x96\x84" - "\xe2\x96\x86" - "\xe2\x96\x88" - "\033[0m"); + return LOG_STR("\033[0;32m" // green + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); } else if (rssi >= -65) { - sprintf(buf, "\033[0;33m" // yellow - "\xe2\x96\x82" - "\xe2\x96\x84" - "\xe2\x96\x86" - "\033[0;37m" - "\xe2\x96\x88" - "\033[0m"); + return LOG_STR("\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\033[0;37m" + "\xe2\x96\x88" + "\033[0m"); } else if (rssi >= -85) { - sprintf(buf, "\033[0;33m" // yellow - "\xe2\x96\x82" - "\xe2\x96\x84" - "\033[0;37m" - "\xe2\x96\x86" - "\xe2\x96\x88" - "\033[0m"); + return LOG_STR("\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\033[0;37m" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); } else { - sprintf(buf, "\033[0;31m" // red - "\xe2\x96\x82" - "\033[0;37m" - "\xe2\x96\x84" - "\xe2\x96\x86" - "\xe2\x96\x88" - "\033[0m"); + return LOG_STR("\033[0;31m" // red + "\xe2\x96\x82" + "\033[0;37m" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); } } void WiFiComponent::print_connect_params_() { - uint8_t bssid[6] = {}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) - memcpy(bssid, raw_bssid, sizeof(bssid)); + bssid_t bssid = wifi_bssid(); - ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), WiFi.SSID().c_str()); - ESP_LOGCONFIG(TAG, " IP Address: %s", WiFi.localIP().toString().c_str()); + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_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()); - char signal_bars[50]; - int8_t rssi = WiFi.RSSI(); - print_signal_bars(rssi, signal_bars); - ESP_LOGCONFIG(TAG, " Signal strength: %d dB %s", rssi, signal_bars); + int8_t rssi = wifi_rssi(); + ESP_LOGCONFIG(TAG, " Signal strength: %d dB %s", rssi, LOG_STR_ARG(get_signal_bars(rssi))); if (this->selected_ap_.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); } - ESP_LOGCONFIG(TAG, " Channel: %d", WiFi.channel()); - ESP_LOGCONFIG(TAG, " Subnet: %s", WiFi.subnetMask().toString().c_str()); - ESP_LOGCONFIG(TAG, " Gateway: %s", WiFi.gatewayIP().toString().c_str()); - ESP_LOGCONFIG(TAG, " DNS1: %s", WiFi.dnsIP(0).toString().c_str()); - ESP_LOGCONFIG(TAG, " DNS2: %s", WiFi.dnsIP(1).toString().c_str()); + ESP_LOGCONFIG(TAG, " Channel: %d", wifi_channel_()); + ESP_LOGCONFIG(TAG, " Subnet: %s", wifi_subnet_mask_().str().c_str()); + ESP_LOGCONFIG(TAG, " Gateway: %s", wifi_gateway_ip_().str().c_str()); + ESP_LOGCONFIG(TAG, " DNS1: %s", wifi_dns_ip_(0).str().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", wifi_dns_ip_(1).str().c_str()); } void WiFiComponent::start_scanning() { @@ -418,16 +425,15 @@ void WiFiComponent::check_scanning_finished() { char bssid_s[18]; auto bssid = res.get_bssid(); sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); - char signal_bars[50]; - print_signal_bars(res.get_rssi(), signal_bars); if (res.get_matches()) { ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), - res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, signal_bars); + res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); ESP_LOGD(TAG, " Channel: %u", res.get_channel()); ESP_LOGD(TAG, " RSSI: %d dB", res.get_rssi()); } else { - ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, signal_bars); + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); } } @@ -463,7 +469,7 @@ void WiFiComponent::check_scanning_finished() { // copy manual IP (if set) connect_params.set_manual_ip(config.get_manual_ip()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // copy EAP parameters (if set) connect_params.set_eap(config.get_eap()); #endif @@ -486,10 +492,10 @@ void WiFiComponent::dump_config() { } void WiFiComponent::check_connecting_finished() { - wl_status_t status = this->wifi_sta_status_(); + auto status = this->wifi_sta_connect_status_(); - if (status == WL_CONNECTED) { - if (WiFi.SSID().equals("")) { + if (status == WiFiSTAConnectStatus::CONNECTED) { + if (wifi_ssid().empty()) { ESP_LOGW(TAG, "Incomplete connection."); this->retry_connect(); return; @@ -513,9 +519,6 @@ void WiFiComponent::check_connecting_finished() { } #endif -#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) - network_setup_mdns(this->wifi_sta_ip_(), 0); -#endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; return; @@ -534,26 +537,23 @@ void WiFiComponent::check_connecting_finished() { return; } - if (status == WL_IDLE_STATUS || status == WL_DISCONNECTED || status == WL_CONNECTION_LOST) { - // WL_DISCONNECTED is set while not connected yet. - // WL_IDLE_STATUS is set while we're waiting for the IP address. - // WL_CONNECTION_LOST happens on the ESP32 + if (status == WiFiSTAConnectStatus::CONNECTING) { return; } - if (status == WL_NO_SSID_AVAIL) { + if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) { ESP_LOGW(TAG, "WiFi network can not be found anymore."); this->retry_connect(); return; } - if (status == WL_CONNECT_FAILED) { + if (status == WiFiSTAConnectStatus::ERROR_CONNECT_FAILED) { ESP_LOGW(TAG, "Connecting to WiFi network failed. Are the credentials wrong?"); this->retry_connect(); return; } - ESP_LOGW(TAG, "WiFi Unknown connection status %d", status); + ESP_LOGW(TAG, "WiFi Unknown connection status %d", (int) status); } void WiFiComponent::retry_connect() { @@ -587,15 +587,15 @@ void WiFiComponent::retry_connect() { } bool WiFiComponent::can_proceed() { - if (this->has_ap() && !this->has_sta()) { + if (!this->has_sta()) { return true; } return this->is_connected(); } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { - return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED && - !this->error_from_callback_; + return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && + this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } @@ -623,16 +623,16 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } -#ifdef ESPHOME_WIFI_WPA2_EAP -void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#ifdef USE_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } -void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = std::move(manual_ip); } +void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } @@ -664,7 +664,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) { if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) return false; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) return false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d690a35420..d04f15695d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,23 +1,24 @@ #pragma once +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include "esphome/components/network/ip_address.h" #include -#include -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO #include #include #include #endif -#ifdef ARDUINO_ARCH_ESP8266 -#include +#ifdef USE_ESP8266 #include +#include -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) extern "C" { #include }; @@ -53,16 +54,24 @@ enum WiFiComponentState { WIFI_COMPONENT_STATE_AP, }; -/// Struct for setting static IPs in WiFiComponent. -struct ManualIP { - IPAddress static_ip; - IPAddress gateway; - IPAddress subnet; - IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. - IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. +enum class WiFiSTAConnectStatus : int { + IDLE, + CONNECTING, + CONNECTED, + ERROR_NETWORK_NOT_FOUND, + ERROR_CONNECT_FAILED, }; -#ifdef ESPHOME_WIFI_WPA2_EAP +/// Struct for setting static IPs in WiFiComponent. +struct ManualIP { + network::IPAddress static_ip; + network::IPAddress gateway; + network::IPAddress subnet; + network::IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. + network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. +}; + +#ifdef USE_WIFI_WPA2_EAP struct EAPAuth { std::string identity; // required for all auth types std::string username; @@ -72,7 +81,7 @@ struct EAPAuth { const char *client_cert; const char *client_key; }; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP using bssid_t = std::array; @@ -82,9 +91,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -92,9 +101,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -104,9 +113,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP optional eap_; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; @@ -152,6 +161,10 @@ enum WiFiPowerSaveMode { WIFI_POWER_SAVE_HIGH, }; +#ifdef USE_ESP_IDF +struct IDFWiFiEvent; +#endif + /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { public: @@ -206,13 +219,13 @@ class WiFiComponent : public Component { bool has_sta() const; bool has_ap() const; - IPAddress get_ip_address(); + network::IPAddress get_ip_address(); std::string get_use_address() const; void set_use_address(const std::string &use_address); const std::vector &get_scan_result() const { return scan_result_; } - IPAddress wifi_soft_ap_ip(); + network::IPAddress wifi_soft_ap_ip(); bool has_sta_priority(const bssid_t &bssid) { for (auto &it : this->sta_priorities_) @@ -238,36 +251,46 @@ class WiFiComponent : public Component { }); } + network::IPAddress wifi_sta_ip(); + std::string wifi_ssid(); + bssid_t wifi_bssid(); + + int8_t wifi_rssi(); + protected: static std::string format_mac_addr(const uint8_t mac[6]); void setup_ap_config_(); void print_connect_params_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); bool wifi_sta_ip_config_(optional manual_ip); - IPAddress wifi_sta_ip_(); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); - wl_status_t wifi_sta_status_(); + WiFiSTAConnectStatus wifi_sta_connect_status_(); bool wifi_scan_start_(); bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); bool wifi_disconnect_(); + int32_t wifi_channel_(); + network::IPAddress wifi_subnet_mask_(); + network::IPAddress wifi_gateway_ip_(); + network::IPAddress wifi_dns_ip_(int num); bool is_captive_portal_active_(); bool is_esp32_improv_active_(); -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); static void s_wifi_scan_done_callback(void *arg, STATUS status); #endif -#ifdef ARDUINO_ARCH_ESP32 +#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 @@ -275,6 +298,9 @@ class WiFiComponent : public Component { #endif void wifi_scan_done_callback_(); #endif +#ifdef USE_ESP_IDF + void wifi_process_event_(IDFWiFiEvent *); +#endif std::string use_address_; std::vector sta_; @@ -282,6 +308,7 @@ class WiFiComponent : public Component { WiFiAP selected_ap_; bool fast_connect_{false}; + bool has_ap_{false}; WiFiAP ap_; WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; uint32_t action_started_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp similarity index 66% rename from esphome/components/wifi/wifi_component_esp32.cpp rename to esphome/components/wifi/wifi_component_esp32_arduino.cpp index c51c17d60c..e1332e3181 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -1,20 +1,21 @@ #include "wifi_component.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO #include #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif #include "lwip/err.h" #include "lwip/dns.h" +#include "lwip/apps/sntp.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/application.h" #include "esphome/core/util.h" @@ -23,32 +24,34 @@ namespace wifi { static const char *const TAG = "wifi_esp32"; +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(); + uint8_t current_mode = WiFiClass::getMode(); bool current_sta = current_mode & 0b01; bool current_ap = current_mode & 0b10; - bool sta_ = sta.value_or(current_sta); - bool ap_ = ap.value_or(current_ap); - if (current_sta == sta_ && current_ap == ap_) + 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 (sta_ && !current_sta) { + if (enable_sta && !current_sta) { ESP_LOGV(TAG, "Enabling STA."); - } else if (!sta_ && current_sta) { + } else if (!enable_sta && current_sta) { ESP_LOGV(TAG, "Disabling STA."); } - if (ap_ && !current_ap) { + if (enable_ap && !current_ap) { ESP_LOGV(TAG, "Enabling AP."); - } else if (!ap_ && current_ap) { + } else if (!enable_ap && current_ap) { ESP_LOGV(TAG, "Disabling AP."); } uint8_t mode = 0; - if (sta_) + if (enable_sta) mode |= 0b01; - if (ap_) + if (enable_ap) mode |= 0b10; - bool ret = WiFi.mode(static_cast(mode)); + bool ret = WiFiClass::mode(static_cast(mode)); if (!ret) { ESP_LOGW(TAG, "Setting WiFi mode failed!"); @@ -92,6 +95,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { 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); @@ -110,8 +118,8 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { 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) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %d", dhcp_stop_ret); + 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); @@ -133,20 +141,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } -IPAddress WiFiComponent::wifi_sta_ip_() { +network::IPAddress WiFiComponent::wifi_sta_ip() { if (!this->has_sta()) - return IPAddress(); + return {}; tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); - return IPAddress(ip.ip.addr); + return {ip.ip.addr}; } bool WiFiComponent::wifi_apply_hostname_() { - esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting hostname failed: %d", err); - return false; - } + // setting is done in SYSTEM_EVENT_STA_START callback return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -154,20 +158,53 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (!this->wifi_mode_(true, {})) return false; + // 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)); - strcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str()); - strcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str()); + strlcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); + strlcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + + // The weakest authmode to accept in the fast scan mode + if (ap.get_password().empty()) { + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; + } else { + conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + } + +#ifdef USE_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; + } +#endif if (ap.get_bssid().has_value()) { - conf.sta.bssid_set = 1; + conf.sta.bssid_set = true; memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); } else { - conf.sta.bssid_set = 0; + conf.sta.bssid_set = false; } if (ap.get_channel().has_value()) { conf.sta.channel = *ap.get_channel(); + conf.sta.scan_method = WIFI_FAST_SCAN; + } else { + conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; } + // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set. + // 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 advertizes 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 + conf.sta.threshold.rssi = -127; + + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; wifi_config_t current_conf; esp_err_t err; @@ -191,7 +228,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP 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(); @@ -235,10 +272,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); + s_sta_connecting = true; + err = esp_wifi_connect(); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_wifi_connect failed! %d", err); @@ -348,39 +387,85 @@ const char *get_disconnect_reason_str(uint8_t reason) { 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"; } } + #if ESP_IDF_VERSION_MAJOR >= 4 -void WiFiComponent::wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info) { -#else -void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) { -#endif + +#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; + +#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 SYSTEM_EVENT_WIFI_READY: { + case ESPHOME_EVENT_ID_WIFI_READY: { ESP_LOGV(TAG, "Event: WiFi ready"); break; } - case SYSTEM_EVENT_SCAN_DONE: { + 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_(); break; } - case SYSTEM_EVENT_STA_START: { + 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()); break; } - case SYSTEM_EVENT_STA_STOP: { + case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "Event: WiFi STA stop"); break; } - case SYSTEM_EVENT_STA_CONNECTED: { + case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_connected; #else @@ -391,9 +476,10 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i 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 SYSTEM_EVENT_STA_DISCONNECTED: { + case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_disconnected; #else @@ -408,9 +494,22 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i 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) { + err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err)); + } + this->error_from_callback_ = true; + } + + s_sta_connecting = false; break; } - case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: { + case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_authmode_change; #else @@ -432,25 +531,26 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i } break; } - case SYSTEM_EVENT_STA_GOT_IP: { + 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(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); + s_sta_connecting = false; break; } - case SYSTEM_EVENT_STA_LOST_IP: { + case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { ESP_LOGV(TAG, "Event: Lost IP"); break; } - case SYSTEM_EVENT_AP_START: { + case ESPHOME_EVENT_ID_WIFI_AP_START: { ESP_LOGV(TAG, "Event: WiFi AP start"); break; } - case SYSTEM_EVENT_AP_STOP: { + case ESPHOME_EVENT_ID_WIFI_AP_STOP: { ESP_LOGV(TAG, "Event: WiFi AP stop"); break; } - case SYSTEM_EVENT_AP_STACONNECTED: { + case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_connected; auto &mac = it.bssid; @@ -461,7 +561,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } - case SYSTEM_EVENT_AP_STADISCONNECTED: { + case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; @@ -472,11 +572,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } - case SYSTEM_EVENT_AP_STAIPASSIGNED: { + case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { ESP_LOGV(TAG, "Event: AP client assigned IP"); break; } - case SYSTEM_EVENT_AP_PROBEREQRECVED: { + case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { #if ESP_IDF_VERSION_MAJOR >= 4 auto it = info.wifi_ap_probereqrecved; #else @@ -488,31 +588,6 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i default: break; } - -#if ESP_IDF_VERSION_MAJOR >= 4 - if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - uint8_t reason = info.wifi_sta_disconnected.reason; -#else - if (event == SYSTEM_EVENT_STA_DISCONNECTED) { - uint8_t reason = info.disconnected.reason; -#endif - 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) { - err_t err = esp_wifi_disconnect(); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err)); - } - this->error_from_callback_ = true; - } - } -#if ESP_IDF_VERSION_MAJOR >= 4 - if (event == ARDUINO_EVENT_WIFI_SCAN_DONE) { -#else - if (event == SYSTEM_EVENT_SCAN_DONE) { -#endif - this->wifi_scan_done_callback_(); - } } void WiFiComponent::wifi_pre_setup_() { auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); @@ -521,7 +596,19 @@ void WiFiComponent::wifi_pre_setup_() { // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } -wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); } +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { + auto status = WiFiClass::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_() { // enable STA if (!this->wifi_mode_(true, {})) @@ -572,9 +659,9 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { info.gw.addr = static_cast(manual_ip->gateway); info.netmask.addr = static_cast(manual_ip->subnet); } else { - info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); - info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); - info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + 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)); } tcpip_adapter_dhcp_status_t dhcp_status; tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); @@ -592,13 +679,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { dhcps_lease_t lease; lease.enable = true; - IPAddress start_address = info.ip.addr; + network::IPAddress start_address = info.ip.addr; start_address[3] += 99; lease.start_ip.addr = static_cast(start_address); - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + 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); - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + 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)); if (err != ESP_OK) { @@ -622,7 +709,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str()); + strlcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -633,9 +720,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str()); + strlcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); } +#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); @@ -651,14 +743,31 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } -IPAddress WiFiComponent::wifi_soft_ap_ip() { +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); - return IPAddress(ip.ip.addr); + return {ip.ip.addr}; } bool WiFiComponent::wifi_disconnect_() { return esp_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 +#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0425f58c28..2021773209 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,12 +1,13 @@ #include "wifi_component.h" +#include "esphome/core/macros.h" -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif @@ -15,14 +16,21 @@ extern "C" { #include "lwip/dns.h" #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 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) +#include "LwipDhcpServer.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 } #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/util.h" #include "esphome/core/application.h" @@ -31,6 +39,12 @@ namespace wifi { static const char *const TAG = "wifi_esp8266"; +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_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) + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { uint8_t current_mode = wifi_get_opmode(); bool current_sta = current_mode & 0b01; @@ -105,6 +119,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { enum dhcp_status dhcp_status = wifi_station_dhcpc_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 != DHCP_STARTED) { bool ret = wifi_station_dhcpc_start(); @@ -164,7 +183,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return ret; } -IPAddress WiFiComponent::wifi_sta_ip_() { +network::IPAddress WiFiComponent::wifi_sta_ip() { if (!this->has_sta()) return {}; struct ip_info ip {}; @@ -219,7 +238,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.bssid_set = 0; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { @@ -243,7 +262,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP 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(); @@ -286,10 +305,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); + // Reset flags, do this _before_ wifi_station_connect as the callback method + // 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; + ETS_UART_INTR_DISABLE(); ret = wifi_station_connect(); ETS_UART_INTR_ENABLE(); @@ -314,20 +341,20 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT }; -const char *get_auth_mode_str(uint8_t mode) { +const LogString *get_auth_mode_str(uint8_t mode) { switch (mode) { case AUTH_OPEN: - return "OPEN"; + return LOG_STR("OPEN"); case AUTH_WEP: - return "WEP"; + return LOG_STR("WEP"); case AUTH_WPA_PSK: - return "WPA PSK"; + return LOG_STR("WPA PSK"); case AUTH_WPA2_PSK: - return "WPA2 PSK"; + return LOG_STR("WPA2 PSK"); case AUTH_WPA_WPA2_PSK: - return "WPA/WPA2 PSK"; + return LOG_STR("WPA/WPA2 PSK"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } #ifdef ipv4_addr @@ -345,79 +372,89 @@ std::string format_ip_addr(struct ip_addr ip) { return buf; } #endif -const char *get_op_mode_str(uint8_t mode) { +const LogString *get_op_mode_str(uint8_t mode) { switch (mode) { case WIFI_OFF: - return "OFF"; + return LOG_STR("OFF"); case WIFI_STA: - return "STA"; + return LOG_STR("STA"); case WIFI_AP: - return "AP"; + return LOG_STR("AP"); case WIFI_AP_STA: - return "AP+STA"; + return LOG_STR("AP+STA"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *get_disconnect_reason_str(uint8_t reason) { + +const LogString *get_disconnect_reason_str(uint8_t reason) { + /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the + * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM + * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. + */ + if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 + switch (reason) { + case REASON_AUTH_EXPIRE: + return LOG_STR("Auth Expired"); + case REASON_AUTH_LEAVE: + return LOG_STR("Auth Leave"); + case REASON_ASSOC_EXPIRE: + return LOG_STR("Association Expired"); + case REASON_ASSOC_TOOMANY: + return LOG_STR("Too Many Associations"); + case REASON_NOT_AUTHED: + return LOG_STR("Not Authenticated"); + case REASON_NOT_ASSOCED: + return LOG_STR("Not Associated"); + case REASON_ASSOC_LEAVE: + return LOG_STR("Association Leave"); + case REASON_ASSOC_NOT_AUTHED: + return LOG_STR("Association not Authenticated"); + case REASON_DISASSOC_PWRCAP_BAD: + return LOG_STR("Disassociate Power Cap Bad"); + case REASON_DISASSOC_SUPCHAN_BAD: + return LOG_STR("Disassociate Supported Channel Bad"); + case REASON_IE_INVALID: + return LOG_STR("IE Invalid"); + case REASON_MIC_FAILURE: + return LOG_STR("Mic Failure"); + case REASON_4WAY_HANDSHAKE_TIMEOUT: + return LOG_STR("4-Way Handshake Timeout"); + case REASON_GROUP_KEY_UPDATE_TIMEOUT: + return LOG_STR("Group Key Update Timeout"); + case REASON_IE_IN_4WAY_DIFFERS: + return LOG_STR("IE In 4-Way Handshake Differs"); + case REASON_GROUP_CIPHER_INVALID: + return LOG_STR("Group Cipher Invalid"); + case REASON_PAIRWISE_CIPHER_INVALID: + return LOG_STR("Pairwise Cipher Invalid"); + case REASON_AKMP_INVALID: + return LOG_STR("AKMP Invalid"); + case REASON_UNSUPP_RSN_IE_VERSION: + return LOG_STR("Unsupported RSN IE version"); + case REASON_INVALID_RSN_IE_CAP: + return LOG_STR("Invalid RSN IE Cap"); + case REASON_802_1X_AUTH_FAILED: + return LOG_STR("802.1x Authentication Failed"); + case REASON_CIPHER_SUITE_REJECTED: + return LOG_STR("Cipher Suite Rejected"); + } + } + switch (reason) { - case REASON_AUTH_EXPIRE: - return "Auth Expired"; - case REASON_AUTH_LEAVE: - return "Auth Leave"; - case REASON_ASSOC_EXPIRE: - return "Association Expired"; - case REASON_ASSOC_TOOMANY: - return "Too Many Associations"; - case REASON_NOT_AUTHED: - return "Not Authenticated"; - case REASON_NOT_ASSOCED: - return "Not Associated"; - case REASON_ASSOC_LEAVE: - return "Association Leave"; - case REASON_ASSOC_NOT_AUTHED: - return "Association not Authenticated"; - case REASON_DISASSOC_PWRCAP_BAD: - return "Disassociate Power Cap Bad"; - case REASON_DISASSOC_SUPCHAN_BAD: - return "Disassociate Supported Channel Bad"; - case REASON_IE_INVALID: - return "IE Invalid"; - case REASON_MIC_FAILURE: - return "Mic Failure"; - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return "4-Way Handshake Timeout"; - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return "Group Key Update Timeout"; - case REASON_IE_IN_4WAY_DIFFERS: - return "IE In 4-Way Handshake Differs"; - case REASON_GROUP_CIPHER_INVALID: - return "Group Cipher Invalid"; - case REASON_PAIRWISE_CIPHER_INVALID: - return "Pairwise Cipher Invalid"; - case REASON_AKMP_INVALID: - return "AKMP Invalid"; - case REASON_UNSUPP_RSN_IE_VERSION: - return "Unsupported RSN IE version"; - case REASON_INVALID_RSN_IE_CAP: - return "Invalid RSN IE Cap"; - case REASON_802_1X_AUTH_FAILED: - return "802.1x Authentication Failed"; - case REASON_CIPHER_SUITE_REJECTED: - return "Cipher Suite Rejected"; case REASON_BEACON_TIMEOUT: - return "Beacon Timeout"; + return LOG_STR("Beacon Timeout"); case REASON_NO_AP_FOUND: - return "AP Not Found"; + return LOG_STR("AP Not Found"); case REASON_AUTH_FAIL: - return "Authentication Failed"; + return LOG_STR("Authentication Failed"); case REASON_ASSOC_FAIL: - return "Association Failed"; + return LOG_STR("Association Failed"); case REASON_HANDSHAKE_TIMEOUT: - return "Handshake Failed"; + return LOG_STR("Handshake Failed"); case REASON_UNSPECIFIED: default: - return "Unspecified"; + return LOG_STR("Unspecified"); } } @@ -430,6 +467,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel); + s_sta_connected = true; break; } case EVENT_STAMODE_DISCONNECTED: { @@ -439,16 +477,20 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { buf[it.ssid_len] = '\0'; if (it.reason == REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + s_sta_connect_not_found = true; } 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)); + format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + s_sta_connect_error = true; } + s_sta_connected = false; + s_sta_connecting = false; break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { auto it = event->event_info.auth_change; - ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)), + LOG_STR_ARG(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 != AUTH_OPEN && it.new_mode == AUTH_OPEN) { @@ -464,6 +506,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { auto it = event->event_info.got_ip; ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); + s_sta_got_ip = true; break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -485,11 +528,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; - ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode), - get_op_mode_str(it.new_opmode)); + ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), + LOG_STR_ARG(get_op_mode_str(it.new_opmode))); break; } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { @@ -540,25 +583,26 @@ void WiFiComponent::wifi_pre_setup_() { this->wifi_mode_(false, false); } -wl_status_t WiFiComponent::wifi_sta_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { station_status_t status = wifi_station_get_connect_status(); switch (status) { case STATION_GOT_IP: - return WL_CONNECTED; + return WiFiSTAConnectStatus::CONNECTED; case STATION_NO_AP_FOUND: - return WL_NO_SSID_AVAIL; + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + ; case STATION_CONNECT_FAIL: case STATION_WRONG_PASSWORD: - return WL_CONNECT_FAILED; - case STATION_IDLE: - return WL_IDLE_STATUS; + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; case STATION_CONNECTING: + return WiFiSTAConnectStatus::CONNECTING; + case STATION_IDLE: default: - return WL_DISCONNECTED; + return WiFiSTAConnectStatus::IDLE; } } bool WiFiComponent::wifi_scan_start_() { - static bool FIRST_SCAN = false; + static bool first_scan = false; // enable STA if (!this->wifi_mode_(true, {})) @@ -570,9 +614,9 @@ bool WiFiComponent::wifi_scan_start_() { config.bssid = nullptr; config.channel = 0; config.show_hidden = 1; -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = WIFI_SCAN_TYPE_ACTIVE; - if (FIRST_SCAN) { + if (first_scan) { config.scan_time.active.min = 100; config.scan_time.active.max = 200; } else { @@ -580,7 +624,7 @@ bool WiFiComponent::wifi_scan_start_() { config.scan_time.active.max = 500; } #endif - FIRST_SCAN = false; + first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed!"); @@ -633,9 +677,9 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { info.gw.addr = static_cast(manual_ip->gateway); info.netmask.addr = static_cast(manual_ip->subnet); } else { - info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); - info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); - info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + 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)); } if (wifi_softap_dhcps_status() == DHCP_STARTED) { @@ -649,14 +693,18 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return false; } +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) + dhcpSoftAP.begin(&info); +#endif + struct dhcps_lease lease {}; - IPAddress start_address = info.ip.addr; + network::IPAddress start_address = info.ip.addr; start_address[3] += 99; lease.start_ip.addr = static_cast(start_address); - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + 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); - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + 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!"); return false; @@ -719,11 +767,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } -IPAddress WiFiComponent::wifi_soft_ap_ip() { +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { struct ip_info ip {}; wifi_get_ip_info(SOFTAP_IF, &ip); return {ip.ip.addr}; } +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 diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp new file mode 100644 index 0000000000..7f71b7078c --- /dev/null +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -0,0 +1,910 @@ +#include "wifi_component.h" + +#ifdef USE_ESP_IDF + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#ifdef USE_WIFI_WPA2_EAP +#include +#endif +#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_esp32"; + +static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static xQueueHandle 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) + +struct IDFWiFiEvent { + esp_event_base_t event_base; + int32_t event_id; + union { + wifi_event_sta_scan_done_t sta_scan_done; + wifi_event_sta_connected_t sta_connected; + wifi_event_sta_disconnected_t sta_disconnected; + wifi_event_sta_authmode_change_t sta_authmode_change; + wifi_event_ap_staconnected_t ap_staconnected; + wifi_event_ap_stadisconnected_t ap_stadisconnected; + 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; + ip_event_ap_staipassigned_t ip_ap_staipassigned; + } data; +}; + +// general design: event handler translates events and pushes them to a queue, +// events get processed in the main loop +void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + IDFWiFiEvent event; + memset(&event, 0, sizeof(IDFWiFiEvent)); + event.event_base = event_base; + event.event_id = event_id; + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + // no data + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_STOP) { + // no data + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { + memcpy(&event.data.sta_authmode_change, event_data, sizeof(wifi_event_sta_authmode_change_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + memcpy(&event.data.sta_connected, event_data, sizeof(wifi_event_sta_connected_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + 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)); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { + // no data + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { + memcpy(&event.data.sta_scan_done, event_data, sizeof(wifi_event_sta_scan_done_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { + // no data + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP) { + // no data + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_PROBEREQRECVED) { + memcpy(&event.data.ap_probe_req_rx, event_data, sizeof(wifi_event_ap_probe_req_rx_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) { + memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) { + memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t)); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) { + memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t)); + } else { + // did not match any event, don't send anything + return; + } + + // copy to heap to keep queue object small + auto *to_send = new IDFWiFiEvent; // NOLINT(cppcoreguidelines-owning-memory) + memcpy(to_send, &event, sizeof(IDFWiFiEvent)); + // don't block, we may miss events but the core can handle that + if (xQueueSend(s_event_queue, &to_send, 0L) != pdPASS) { + delete to_send; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +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 + esp_err_t err = esp_netif_init(); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err)); + return; + } + s_wifi_event_group = xEventGroupCreate(); + if (s_wifi_event_group == nullptr) { + ESP_LOGE(TAG, "xEventGroupCreate failed"); + return; + } + // NOLINTNEXTLINE(bugprone-sizeof-expression) + s_event_queue = xQueueCreate(64, sizeof(IDFWiFiEvent *)); + if (s_event_queue == nullptr) { + ESP_LOGE(TAG, "xQueueCreate failed"); + return; + } + err = esp_event_loop_create_default(); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); + return; + } + esp_event_handler_instance_t instance_wifi_id, instance_ip_id; + err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err)); + return; + } + err = esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_ip_id); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err)); + return; + } + + s_sta_netif = esp_netif_create_default_wifi_sta(); + s_ap_netif = esp_netif_create_default_wifi_ap(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // cfg.nvs_enable = false; + err = esp_wifi_init(&cfg); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); + return; + } + err = esp_wifi_set_storage(WIFI_STORAGE_RAM); + if (err != ERR_OK) { + ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err)); + return; + } +} + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + esp_err_t err; + wifi_mode_t current_mode = WIFI_MODE_NULL; + if (s_wifi_started) { + err = esp_wifi_get_mode(¤t_mode); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_wifi_get_mode failed: %s", esp_err_to_name(err)); + return false; + } + } + 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; + + 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 (set_sta && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!set_sta && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + } + if (set_ap && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!set_ap && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + if (set_mode == WIFI_MODE_NULL && s_wifi_started) { + err = esp_wifi_stop(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_stop failed: %s", esp_err_to_name(err)); + return false; + } + s_wifi_started = false; + return true; + } + + err = esp_wifi_set_mode(set_mode); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); + return false; + } + + if (set_mode != WIFI_MODE_NULL && !s_wifi_started) { + err = esp_wifi_start(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err)); + return false; + } + s_wifi_started = true; + } + + return true; +} + +bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(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_) { + case WIFI_POWER_SAVE_LIGHT: + power_save = WIFI_PS_MIN_MODEM; + break; + case WIFI_POWER_SAVE_HIGH: + power_save = WIFI_PS_MAX_MODEM; + break; + case WIFI_POWER_SAVE_NONE: + default: + power_save = WIFI_PS_NONE; + break; + } + return esp_wifi_set_ps(power_save) == ESP_OK; +} + +bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + // 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)); + + // The weakest authmode to accept in the fast scan mode + if (ap.get_password().empty()) { + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; + } else { + conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + } + +#ifdef USE_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; + } +#endif + + if (ap.get_bssid().has_value()) { + conf.sta.bssid_set = true; + memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); + } else { + conf.sta.bssid_set = false; + } + if (ap.get_channel().has_value()) { + conf.sta.channel = *ap.get_channel(); + conf.sta.scan_method = WIFI_FAST_SCAN; + } else { + conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + } + // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set. + // 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 advertizes 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 + conf.sta.threshold.rssi = -127; + + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; + + wifi_config_t current_conf; + esp_err_t err; + 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) { + err = esp_wifi_disconnect(); + if (err != ESP_OK) { + 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: %s", esp_err_to_name(err)); + return false; + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + // setup enterprise authentication if required +#ifdef USE_WIFI_WPA2_EAP + 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(); + err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_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) { + err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_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 + 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())); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + } + } else { + // in the absence of certs, assume this is username/password based + err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + } + err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + } + } + esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); + err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + } + } +#endif // USE_WIFI_WPA2_EAP + + // Reset flags, do this _before_ wifi_station_connect as the callback method + // 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; + + err = esp_wifi_connect(); + if (err != ESP_OK) { + 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; + + tcpip_adapter_dhcp_status_t dhcp_status; + esp_err_t err = tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcpc_get_status failed: %s", esp_err_to_name(err)); + return false; + } + + if (!manual_ip.has_value()) { + // Use DHCP client + if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { + 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); + + err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcpc_stop failed: %s", esp_err_to_name(err)); + return false; + } + + err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed: %s", esp_err_to_name(err)); + return false; + } + + ip_addr_t dns; + dns.type = IPADDR_TYPE_V4; + if (uint32_t(manual_ip->dns1) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); + dns_setserver(0, &dns); + } + if (uint32_t(manual_ip->dns2) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); + dns_setserver(1, &dns); + } + + return true; +} + +network::IPAddress WiFiComponent::wifi_sta_ip() { + if (!this->has_sta()) + return {}; + tcpip_adapter_ip_info_t ip; + esp_err_t err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_get_ip_info failed: %s", esp_err_to_name(err)); + return false; + } + return {ip.ip.addr}; +} + +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: + 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"; + 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"; + } +} + +std::string format_ip4_addr(const esp_ip4_addr_t &ip) { + char buf[20]; + snprintf(buf, sizeof(buf), "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +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"; + } +} + +void WiFiComponent::wifi_loop_() { + while (true) { + IDFWiFiEvent *data; + if (xQueueReceive(s_event_queue, &data, 0L) != pdTRUE) { + // no event ready + break; + } + + // process event + wifi_process_event_(data); + + delete data; // NOLINT(cppcoreguidelines-owning-memory) + } +} +void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { + esp_err_t err; + if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { + ESP_LOGV(TAG, "Event: WiFi STA start"); + // apply hostname + err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + if (err != ERR_OK) { + ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + } + + s_sta_started = true; + // re-apply power save mode + wifi_apply_power_save_(); + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { + ESP_LOGV(TAG, "Event: WiFi STA stop"); + s_sta_started = false; + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { + const auto &it = data->data.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)); + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { + const auto &it = data->data.sta_connected; + char buf[33]; + assert(it.ssid_len <= 32); + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "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)); + s_sta_connected = true; + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { + const auto &it = data->data.sta_disconnected; + char buf[33]; + assert(it.ssid_len <= 32); + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + if (it.reason == WIFI_REASON_NO_AP_FOUND) { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + s_sta_connect_not_found = true; + + } 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)); + s_sta_connect_error = true; + } + s_sta_connected = false; + s_sta_connecting = false; + error_from_callback_ = true; + + } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { + const auto &it = data->data.ip_got_ip; + 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; + + } 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; + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { + const auto &it = data->data.sta_scan_done; + ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + + scan_result_.clear(); + this->scan_done_ = true; + if (it.status != 0) { + // scan error + return; + } + + uint16_t number = it.number; + std::vector records(number); + err = esp_wifi_scan_get_ap_records(&number, records.data()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); + return; + } + records.resize(number); + + scan_result_.reserve(number); + for (int i = 0; i < number; i++) { + auto &record = records[i]; + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + std::string ssid(reinterpret_cast(record.ssid)); + WiFiScanResult result(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); + scan_result_.push_back(result); + } + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { + ESP_LOGV(TAG, "Event: WiFi AP start"); + s_ap_started = true; + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { + ESP_LOGV(TAG, "Event: WiFi AP stop"); + s_ap_started = false; + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { + const auto &it = data->data.ap_probe_req_rx; + ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { + const auto &it = data->data.ap_staconnected; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); + + } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { + const auto &it = data->data.ap_stadisconnected; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); + + } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { + const auto &it = data->data.ip_ap_staipassigned; + ESP_LOGV(TAG, "Event: AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + } +} + +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { + if (s_sta_connected && s_sta_got_ip) { + return WiFiSTAConnectStatus::CONNECTED; + } + if (s_sta_connect_error) { + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + } + if (s_sta_connect_not_found) { + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + } + if (s_sta_connecting) { + return WiFiSTAConnectStatus::CONNECTING; + } + return WiFiSTAConnectStatus::IDLE; +} +bool WiFiComponent::wifi_scan_start_() { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + wifi_scan_config_t config{}; + config.ssid = nullptr; + config.bssid = nullptr; + config.channel = 0; + config.show_hidden = true; + config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + config.scan_time.active.min = 100; + config.scan_time.active.max = 300; + + esp_err_t err = esp_wifi_scan_start(&config, false); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_scan_start failed: %s", esp_err_to_name(err)); + return false; + } + + scan_done_ = false; + return true; +} +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + esp_err_t err; + + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + tcpip_adapter_ip_info_t info; + memset(&info, 0, sizeof(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); + } 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)); + } + 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); + return false; + } + + err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_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); + 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); + 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)); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + return false; + } + + err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + return false; + } + + return true; +} +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + wifi_config_t conf; + memset(&conf, 0, sizeof(conf)); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + conf.ap.channel = ap.get_channel().value_or(1); + conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.max_connection = 5; + conf.ap.beacon_interval = 100; + + if (ap.get_password().empty()) { + conf.ap.authmode = WIFI_AUTH_OPEN; + *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.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); + return false; + } + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + 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}; +} +bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } + +bssid_t WiFiComponent::wifi_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; + } + std::copy(info.bssid, info.bssid + 6, res.begin()); + return res; +} +std::string WiFiComponent::wifi_ssid() { + wifi_ap_record_t info{}; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return ""; + } + auto *ssid_s = reinterpret_cast(info.ssid); + size_t len = strnlen(ssid_s, sizeof(info.ssid)); + return {ssid_s, len}; +} +int8_t WiFiComponent::wifi_rssi() { + wifi_ap_record_t info; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return 0; + } + return info.rssi; +} +int32_t WiFiComponent::wifi_channel_() { + uint8_t primary; + wifi_second_chan_t second; + esp_err_t err = esp_wifi_get_channel(&primary, &second); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_get_channel failed: %s", esp_err_to_name(err)); + return 0; + } + return primary; +} +network::IPAddress WiFiComponent::wifi_subnet_mask_() { + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + return {}; + } + return {ip.netmask.addr}; +} +network::IPAddress WiFiComponent::wifi_gateway_ip_() { + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + return {}; + } + return {ip.gw.addr}; +} +network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { + const ip_addr_t *dns_ip = dns_getserver(num); + return {dns_ip->u_addr.ip4.addr}; +} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 071737ccd7..3cb60e6175 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -57,6 +57,7 @@ def wrapped_load_pem_private_key(value, password): def read_relative_config_path(value): + # pylint: disable=unspecified-encoding return Path(CORE.relative_config_path(value)).read_text() diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 50ec3eb272..1922502204 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, + CONF_SCAN_RESULTS, CONF_SSID, CONF_MAC_ADDRESS, ) @@ -15,6 +16,9 @@ wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component ) +ScanResultsWiFiInfo = wifi_info_ns.class_( + "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent +) SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_( "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component @@ -30,6 +34,11 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo), } ), + cv.Optional(CONF_SCAN_RESULTS): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ScanResultsWiFiInfo), + } + ).extend(cv.polling_component_schema("60s")), cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(SSIDWiFiInfo), @@ -62,3 +71,4 @@ async def to_code(config): 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) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 92e5d93a5a..0b73de68de 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -7,6 +7,7 @@ namespace wifi_info { static const char *const TAG = "wifi_info"; void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Results", this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 6d2be08fa0..5b54451ed0 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,10 +10,10 @@ namespace wifi_info { class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: void loop() override { - IPAddress ip = WiFi.localIP(); + auto ip = wifi::global_wifi_component->wifi_sta_ip(); if (ip != this->last_ip_) { this->last_ip_ = ip; - this->publish_state(ip.toString().c_str()); + this->publish_state(ip.str()); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -21,15 +21,44 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { void dump_config() override; protected: - IPAddress last_ip_; + network::IPAddress last_ip_; +}; + +class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { + public: + void update() override { + std::string scan_results; + for (auto &scan : wifi::global_wifi_component->get_scan_result()) { + if (scan.get_is_hidden()) + continue; + + scan_results += scan.get_ssid(); + scan_results += ": "; + scan_results += esphome::to_string(scan.get_rssi()); + scan_results += "dB\n"; + } + + if (this->last_scan_results_ != scan_results) { + this->last_scan_results_ = scan_results; + // There's a limit of 255 characters per state. + // Longer states just don't get sent so we truncate it. + this->publish_state(scan_results.substr(0, 255)); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-scanresults"; } + void dump_config() override; + + protected: + std::string last_scan_results_; }; class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: void loop() override { - String ssid = WiFi.SSID(); - if (this->last_ssid_ != ssid.c_str()) { - this->last_ssid_ = std::string(ssid.c_str()); + std::string ssid = wifi::global_wifi_component->wifi_ssid(); + if (this->last_ssid_ != ssid) { + this->last_ssid_ = ssid; this->publish_state(this->last_ssid_); } } @@ -44,9 +73,9 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: void loop() override { - uint8_t *bssid = WiFi.BSSID(); - if (memcmp(bssid, this->last_bssid_.data(), 6) != 0) { - std::copy(bssid, bssid + 6, this->last_bssid_.data()); + wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); + if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { + std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); char buf[30]; sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); this->publish_state(buf); diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index f1807966a2..37bee75928 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) @@ -17,11 +16,10 @@ WiFiSignalSensor = wifi_signal_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index 8fe108a530..f797aaa590 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -10,7 +10,7 @@ namespace wifi_signal { class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { public: - void update() override { this->publish_state(WiFi.RSSI()); } + void update() override { this->publish_state(wifi::global_wifi_component->wifi_rssi()); } void dump_config() override; std::string unique_id() override { return get_mac_address() + "-wifisignal"; } diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index c9e23bb7eb..2795529203 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -7,7 +7,7 @@ from esphome.const import CONF_NAME, CONF_PORT wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) -CONFIG_SCHEMA = cv.Schema({}) +CONFIG_SCHEMA = cv.All(cv.Schema({}), cv.only_with_arduino) @register_addressable_effect( diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index afff956c9c..8c68bca6e3 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -1,11 +1,14 @@ +#ifdef USE_ARDUINO + #include "wled_light_effect.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #endif -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 #include #include #endif @@ -40,14 +43,15 @@ void WLEDLightEffect::stop() { void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { - it[led].set(COLOR_BLACK); + it[led].set(Color::BLACK); } + it.schedule_show(); } void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { // Init UDP lazily if (!udp_) { - udp_.reset(new WiFiUDP()); + udp_ = make_unique(); if (!udp_->begin(port_)) { ESP_LOGW(TAG, "Cannot bind WLEDLightEffect to %d.", port_); @@ -134,6 +138,7 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p blank_at_ = millis() + DEFAULT_BLANK_TIME; } + it.schedule_show(); return true; } @@ -243,3 +248,5 @@ bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint } // namespace wled } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 2a7654ec27..f0021ca978 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ARDUINO + #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" @@ -39,3 +41,5 @@ class WLEDLightEffect : public light::AddressableLightEffect { } // namespace wled } // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index c736a236a1..884969f793 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 #include #include "mbedtls/ccm.h" @@ -15,7 +15,7 @@ static const char *const TAG = "xiaomi_ble"; bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { // motion detection, 1 byte, 8-bit unsigned integer if ((value_type == 0x03) && (value_length == 1)) { - result.has_motion = (data[0]) ? true : false; + result.has_motion = data[0]; } // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C else if ((value_type == 0x04) && (value_length == 2)) { @@ -31,7 +31,7 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); result.illuminance = illuminance; - result.is_light = (illuminance == 100) ? true : false; + result.is_light = illuminance == 100; if (value_type == 0x0F) result.has_motion = true; } @@ -62,7 +62,7 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l } // on/off state, 1 byte, 8-bit unsigned integer else if ((value_type == 0x12) && (value_length == 1)) { - result.is_active = (data[0]) ? true : false; + result.is_active = data[0]; } // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % else if ((value_type == 0x13) && (value_length == 1)) { @@ -72,7 +72,7 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l else if ((value_type == 0x17) && (value_length == 4)) { const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]); result.idle_time = idle_time / 60.0f; - result.has_motion = (idle_time) ? false : true; + result.has_motion = !idle_time; } else { return false; } @@ -81,7 +81,7 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l } bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { - result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status + result.has_encryption = message[0] & 0x08; // update encryption status if (result.has_encryption) { ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); return false; @@ -104,7 +104,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -136,9 +136,9 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } auto raw = service_data.data; - result.has_data = (raw[0] & 0x40) ? true : false; - result.has_capability = (raw[0] & 0x20) ? true : false; - result.has_encryption = (raw[0] & 0x08) ? true : false; + result.has_data = raw[0] & 0x40; + result.has_capability = raw[0] & 0x20; + result.has_encryption = raw[0] & 0x08; if (!result.has_data) { ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); @@ -203,6 +203,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display result.type = XiaomiParseResult::TYPE_MHOC401; result.name = "MHOC401"; + } else if ((raw[2] == 0x83) && (raw[3] == 0x0A)) { // Qingping-branded, motion & ambient light sensor + result.type = XiaomiParseResult::TYPE_CGPR1; + result.name = "CGPR1"; + if (raw.size() == 19) + result.raw_offset -= 6; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; @@ -344,9 +349,9 @@ bool report_xiaomi_results(const optional &result, const std: bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() // and then again by the respective device class's parse_device() function. Parsing the header - // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // here and then for each device seems to be unnecessary and complicates the duplicate packet filtering. // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely - // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // in the respective device instance. The XiaomiListener class is defined in __init__.py and I was not // able to remove it entirely. return false; // with true it's not showing device scans diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index f431eca11e..54ab9a144f 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -3,7 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_ble { @@ -23,7 +23,8 @@ struct XiaomiParseResult { TYPE_MUE4094RT, TYPE_WX08ZM, TYPE_MJYD02YLA, - TYPE_MHOC401 + TYPE_MHOC401, + TYPE_CGPR1 } type; std::string name; optional temperature; diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py index e7f18a6be9..774c87fee9 100644 --- a/esphome/components/xiaomi_cgd1/sensor.py +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp index c6e7a3f962..97bbd6e6d6 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -1,7 +1,7 @@ #include "xiaomi_cgd1.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgd1 { @@ -52,11 +52,7 @@ bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiCGD1::set_bindkey(const std::string &bindkey) { @@ -67,7 +63,7 @@ void XiaomiCGD1::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index b9e05f857c..d05cffc4d1 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgd1 { diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py index 6b2c144911..d4e7230fd0 100644 --- a/esphome/components/xiaomi_cgdk2/sensor.py +++ b/esphome/components/xiaomi_cgdk2/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp index 2ba2ac4c0a..a97ca93206 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp @@ -1,7 +1,7 @@ #include "xiaomi_cgdk2.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgdk2 { @@ -52,11 +52,7 @@ bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiCGDK2::set_bindkey(const std::string &bindkey) { @@ -67,7 +63,7 @@ void XiaomiCGDK2::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 70f2ae9e2e..8fd9946537 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgdk2 { diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py index f26a7ae54e..4e606d95f8 100644 --- a/esphome/components/xiaomi_cgg1/sensor.py +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 86192fb028..e1f83e4ddd 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -1,7 +1,7 @@ #include "xiaomi_cgg1.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgg1 { @@ -52,11 +52,7 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiCGG1::set_bindkey(const std::string &bindkey) { @@ -67,7 +63,7 @@ void XiaomiCGG1::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index e1d812e929..966c05ac79 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_cgg1 { diff --git a/esphome/components/xiaomi_cgpr1/__init__.py b/esphome/components/xiaomi_cgpr1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py new file mode 100644 index 0000000000..a7f6c41225 --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BINDKEY, + CONF_DEVICE_CLASS, + CONF_MAC_ADDRESS, + CONF_ID, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + ICON_EMPTY, + UNIT_PERCENT, + CONF_IDLE_TIME, + CONF_ILLUMINANCE, + UNIT_MINUTE, + UNIT_LUX, + ICON_TIMELAPSE, +) + +DEPENDENCIES = ["esp32_ble_tracker"] +AUTO_LOAD = ["xiaomi_ble", "sensor"] + +xiaomi_cgpr1_ns = cg.esphome_ns.namespace("xiaomi_cgpr1") +XiaomiCGPR1 = xiaomi_cgpr1_ns.class_( + "XiaomiCGPR1", + binary_sensor.BinarySensor, + cg.Component, + esp32_ble_tracker.ESPBTDeviceListener, +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(XiaomiCGPR1), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional( + CONF_DEVICE_CLASS, default="motion" + ): binary_sensor.device_class, + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY + ), + cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( + UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_IDLE_TIME in config: + sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + cg.add(var.set_idle_time(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp new file mode 100644 index 0000000000..db63beea89 --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp @@ -0,0 +1,75 @@ +#include "xiaomi_cgpr1.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_cgpr1 { + +static const char *const TAG = "xiaomi_cgpr1"; + +void XiaomiCGPR1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGPR1"); + LOG_BINARY_SENSOR(" ", "Motion", this); + LOG_SENSOR(" ", "Idle Time", this->idle_time_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +bool XiaomiCGPR1::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->idle_time.has_value() && this->idle_time_ != nullptr) + this->idle_time_->publish_state(*res->idle_time); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + if (res->has_motion.has_value()) + this->publish_state(*res->has_motion); + success = true; + } + + return success; +} + +void XiaomiCGPR1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_cgpr1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h new file mode 100644 index 0000000000..eff4b1c6fb --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_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_cgpr1 { + +class XiaomiCGPR1 : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { 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_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *idle_time_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace xiaomi_cgpr1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py index a5c702aa9d..4154b64233 100644 --- a/esphome/components/xiaomi_gcls002/sensor.py +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -35,32 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiGCLS002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + 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_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp index 5f7d67b85a..990346e01e 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -1,7 +1,7 @@ #include "xiaomi_gcls002.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_gcls002 { @@ -53,11 +53,7 @@ bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_gcls002 diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index d800e2837d..08e1bd7e54 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_gcls002 { diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 03289a6219..1818731a0f 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -37,39 +35,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + 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_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 103201d511..30990b121d 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -1,7 +1,7 @@ #include "xiaomi_hhccjcy01.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_hhccjcy01 { @@ -56,11 +56,7 @@ bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index bd9d742b2d..aa99cc004a 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_hhccjcy01 { diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py index 8393de5e5a..82ee12d8d1 100644 --- a/esphome/components/xiaomi_hhccpot002/sensor.py +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_WATER_PERCENT, @@ -28,18 +27,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp index efc83cb6cc..3ae29088bb 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -1,7 +1,7 @@ #include "xiaomi_hhccpot002.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_hhccpot002 { @@ -47,11 +47,7 @@ bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_hhccpot002 diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index 1add8e27b1..ce746b9ee0 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_hhccpot002 { diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py index 70036eb5d9..40991c3d0f 100644 --- a/esphome/components/xiaomi_jqjcy01ym/sensor.py +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -7,10 +7,8 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,32 +32,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MILLIGRAMS_PER_CUBIC_METER, - ICON_FLASK_OUTLINE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK_OUTLINE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp index c88a3c3b61..1efebc2849 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -1,7 +1,7 @@ #include "xiaomi_jqjcy01ym.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_jqjcy01ym { @@ -53,11 +53,7 @@ bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_jqjcy01ym diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index d750e1e97f..ca1ad0f27e 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_jqjcy01ym { diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py index ca55f28176..339c5e673a 100644 --- a/esphome/components/xiaomi_lywsd02/sensor.py +++ b/esphome/components/xiaomi_lywsd02/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_BATTERY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSD02), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index 6b8ecdeaff..a6f27c58b9 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -1,7 +1,7 @@ #include "xiaomi_lywsd02.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsd02 { @@ -50,11 +50,7 @@ bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_lywsd02 diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index ec00464cb5..641a02bd5a 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsd02 { diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 05b3798955..f27cee3800 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -34,25 +33,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index f0a2cee8d4..547cc7c114 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -1,7 +1,7 @@ #include "xiaomi_lywsd03mmc.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsd03mmc { @@ -56,11 +56,7 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { @@ -71,7 +67,7 @@ void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index c2828e3cd1..95710a1508 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsd03mmc { diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py index 82bb4c83fb..39a207327e 100644 --- a/esphome/components/xiaomi_lywsdcgq/sensor.py +++ b/esphome/components/xiaomi_lywsdcgq/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 39bcd9df03..749ca83afb 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -1,7 +1,7 @@ #include "xiaomi_lywsdcgq.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsdcgq { @@ -50,11 +50,7 @@ bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_lywsdcgq diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index 553b5965fd..cbc76f9dd3 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_lywsdcgq { diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py index 5180bdbb89..57b2190150 100644 --- a/esphome/components/xiaomi_mhoc401/sensor.py +++ b/esphome/components/xiaomi_mhoc401/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -33,25 +32,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index e93a4b91ae..0cad5c67b2 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -1,7 +1,7 @@ #include "xiaomi_mhoc401.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mhoc401 { @@ -56,11 +56,7 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiMHOC401::set_bindkey(const std::string &bindkey) { @@ -71,7 +67,7 @@ void XiaomiMHOC401::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index e80916f855..4ab882b2af 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mhoc401 { diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py deleted file mode 100644 index 0a0b3ff63f..0000000000 --- a/esphome/components/xiaomi_miflora/sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_hhccjcy01") diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py deleted file mode 100644 index 597d8d1bce..0000000000 --- a/esphome/components/xiaomi_mijia/sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_lywsdcgq") diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 9fe76c0645..517870cc01 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -8,7 +8,9 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_KILOGRAM, ICON_SCALE_BATHROOM, - DEVICE_CLASS_EMPTY, + UNIT_OHM, + CONF_IMPEDANCE, + ICON_OMEGA, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -24,11 +26,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiMiscale), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOGRAM, + icon=ICON_SCALE_BATHROOM, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_OMEGA, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -47,3 +54,6 @@ async def to_code(config): if CONF_WEIGHT in config: sens = await sensor.new_sensor(config[CONF_WEIGHT]) cg.add(var.set_weight(sens)) + if CONF_IMPEDANCE in config: + sens = await sensor.new_sensor(config[CONF_IMPEDANCE]) + cg.add(var.set_impedance(sens)) diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 78464da6e3..de77e6146b 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -1,7 +1,7 @@ #include "xiaomi_miscale.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_miscale { @@ -11,6 +11,7 @@ static const char *const TAG = "xiaomi_miscale"; void XiaomiMiscale::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi Miscale"); LOG_SENSOR(" ", "Weight", this->weight_); + LOG_SENSOR(" ", "Impedance", this->impedance_); } bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { @@ -22,36 +23,58 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool success = false; for (auto &service_data : device.get_service_datas()) { - auto res = parse_header(service_data); + auto res = parse_header_(service_data); if (!res.has_value()) { continue; } - if (!(parse_message(service_data.data, *res))) { + + if (!(parse_message_(service_data.data, *res))) { continue; } - if (!(report_results(res, device.address_str()))) { + + if (!(report_results_(res, device.address_str()))) { continue; } + if (res->weight.has_value() && this->weight_ != nullptr) this->weight_->publish_state(*res->weight); + + if (res->version == 1 && this->impedance_ != nullptr) { + ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as verison 1."); + } else if (res->impedance.has_value() && this->impedance_ != nullptr) + this->impedance_->publish_state(*res->impedance); success = true; } return success; } -optional XiaomiMiscale::parse_header(const esp32_ble_tracker::ServiceData &service_data) { +optional XiaomiMiscale::parse_header_(const esp32_ble_tracker::ServiceData &service_data) { ParseResult result; - if (!service_data.uuid.contains(0x1D, 0x18)) { - ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + if (service_data.uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(0x181D) && service_data.data.size() == 10) { + result.version = 1; + } else if (service_data.uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(0x181B) && service_data.data.size() == 13) { + result.version = 2; + } else { + ESP_LOGVV(TAG, + "parse_header(): Couldn't identify scale version or data size was not correct. UUID: %s, data_size: %d", + service_data.uuid.to_string().c_str(), service_data.data.size()); return {}; } return result; } -bool XiaomiMiscale::parse_message(const std::vector &message, ParseResult &result) { - // exemple 1d18 a2 6036 e307 07 11 0f1f11 +bool XiaomiMiscale::parse_message_(const std::vector &message, ParseResult &result) { + if (result.version == 1) { + return parse_message_v1_(message, result); + } else { + return parse_message_v2_(message, result); + } +} + +bool XiaomiMiscale::parse_message_v1_(const std::vector &message, ParseResult &result) { + // message size is checked in parse_header // 1-2 Weight (MISCALE 181D) // 3-4 Years (MISCALE 181D) // 5 month (MISCALE 181D) @@ -61,36 +84,74 @@ bool XiaomiMiscale::parse_message(const std::vector &message, ParseResu // 9 second (MISCALE 181D) const uint8_t *data = message.data(); - const int data_length = 10; - - if (message.size() != data_length) { - ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); - return false; - } // weight, 2 bytes, 16-bit unsigned integer, 1 kg const int16_t weight = uint16_t(data[1]) | (uint16_t(data[2]) << 8); if (data[0] == 0x22 || data[0] == 0xa2) result.weight = weight * 0.01f / 2.0f; // unit 'kg' else if (data[0] == 0x12 || data[0] == 0xb2) - result.weight = weight * 0.01f * 0.6; // unit 'jin' + result.weight = weight * 0.01f * 0.6f; // unit 'jin' else if (data[0] == 0x03 || data[0] == 0xb3) - result.weight = weight * 0.01f * 0.453592; // unit 'lbs' + result.weight = weight * 0.01f * 0.453592f; // unit 'lbs' return true; } -bool XiaomiMiscale::report_results(const optional &result, const std::string &address) { +bool XiaomiMiscale::parse_message_v2_(const std::vector &message, ParseResult &result) { + // message size is checked in parse_header + // 2-3 Years (MISCALE 2 181B) + // 4 month (MISCALE 2 181B) + // 5 day (MISCALE 2 181B) + // 6 hour (MISCALE 2 181B) + // 7 minute (MISCALE 2 181B) + // 8 second (MISCALE 2 181B) + // 9-10 impedance (MISCALE 2 181B) + // 11-12 weight (MISCALE 2 181B) + + const uint8_t *data = message.data(); + + bool has_impedance = ((data[1] & (1 << 1)) != 0); + bool is_stabilized = ((data[1] & (1 << 5)) != 0); + bool load_removed = ((data[1] & (1 << 7)) != 0); + + if (!is_stabilized || load_removed) { + return false; + } + + // weight, 2 bytes, 16-bit unsigned integer, 1 kg + const int16_t weight = uint16_t(data[11]) | (uint16_t(data[12]) << 8); + if (data[0] == 0x02) + result.weight = weight * 0.01f / 2.0f; // unit 'kg' + else if (data[0] == 0x03) + result.weight = weight * 0.01f * 0.453592f; // unit 'lbs' + + if (has_impedance) { + // impedance, 2 bytes, 16-bit + const int16_t impedance = uint16_t(data[9]) | (uint16_t(data[10]) << 8); + result.impedance = impedance; + + if (impedance == 0 || impedance >= 3000) { + return false; + } + } + + return true; +} + +bool XiaomiMiscale::report_results_(const optional &result, const std::string &address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_results(): no results available."); return false; } - ESP_LOGD(TAG, "Got Xiaomi Miscale (%s):", address.c_str()); + ESP_LOGD(TAG, "Got Xiaomi Miscale v%d (%s):", result->version, address.c_str()); if (result->weight.has_value()) { ESP_LOGD(TAG, " Weight: %.2fkg", *result->weight); } + if (result->impedance.has_value()) { + ESP_LOGD(TAG, " Impedance: %.0fohm", *result->impedance); + } return true; } diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index d9da4f9421..3e51405ddc 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -4,13 +4,15 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_miscale { struct ParseResult { + int version; optional weight; + optional impedance; }; class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceListener { @@ -21,14 +23,18 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } void set_weight(sensor::Sensor *weight) { weight_ = weight; } + void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } protected: uint64_t address_; sensor::Sensor *weight_{nullptr}; + sensor::Sensor *impedance_{nullptr}; - optional parse_header(const esp32_ble_tracker::ServiceData &service_data); - bool parse_message(const std::vector &message, ParseResult &result); - bool report_results(const optional &result, const std::string &address); + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message_(const std::vector &message, ParseResult &result); + bool parse_message_v1_(const std::vector &message, ParseResult &result); + bool parse_message_v2_(const std::vector &message, ParseResult &result); + bool report_results_(const optional &result, const std::string &address); }; } // namespace xiaomi_miscale diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py index 9944098407..de04e8171e 100644 --- a/esphome/components/xiaomi_miscale2/sensor.py +++ b/esphome/components/xiaomi_miscale2/sensor.py @@ -1,58 +1,5 @@ -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_ID, - CONF_WEIGHT, - STATE_CLASS_MEASUREMENT, - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - UNIT_OHM, - CONF_IMPEDANCE, - ICON_OMEGA, - DEVICE_CLASS_EMPTY, + +CONFIG_SCHEMA = cv.invalid( + "This platform has been combined into xiaomi_miscale. Use xiaomi_miscale instead." ) - -DEPENDENCIES = ["esp32_ble_tracker"] - -xiaomi_miscale2_ns = cg.esphome_ns.namespace("xiaomi_miscale2") -XiaomiMiscale2 = xiaomi_miscale2_ns.class_( - "XiaomiMiscale2", esp32_ble_tracker.ESPBTDeviceListener, cg.Component -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XiaomiMiscale2), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( - UNIT_OHM, ICON_OMEGA, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT - ), - } - ) - .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 CONF_WEIGHT in config: - sens = await sensor.new_sensor(config[CONF_WEIGHT]) - cg.add(var.set_weight(sens)) - if CONF_IMPEDANCE in config: - sens = await sensor.new_sensor(config[CONF_IMPEDANCE]) - cg.add(var.set_impedance(sens)) diff --git a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp deleted file mode 100644 index cc63e46573..0000000000 --- a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "xiaomi_miscale2.h" -#include "esphome/core/log.h" - -#ifdef ARDUINO_ARCH_ESP32 - -namespace esphome { -namespace xiaomi_miscale2 { - -static const char *const TAG = "xiaomi_miscale2"; - -void XiaomiMiscale2::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Miscale2"); - LOG_SENSOR(" ", "Weight", this->weight_); - LOG_SENSOR(" ", "Impedance", this->impedance_); -} - -bool XiaomiMiscale2::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 = parse_header(service_data); - if (!res.has_value()) { - continue; - } - if (!(parse_message(service_data.data, *res))) { - continue; - } - if (!(report_results(res, device.address_str()))) { - continue; - } - if (res->weight.has_value() && this->weight_ != nullptr) - this->weight_->publish_state(*res->weight); - if (res->impedance.has_value() && this->impedance_ != nullptr) - this->impedance_->publish_state(*res->impedance); - success = true; - } - - return success; -} - -optional XiaomiMiscale2::parse_header(const esp32_ble_tracker::ServiceData &service_data) { - ParseResult result; - if (!service_data.uuid.contains(0x1B, 0x18)) { - ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); - return {}; - } - - return result; -} - -bool XiaomiMiscale2::parse_message(const std::vector &message, ParseResult &result) { - // 2-3 Years (MISCALE 2 181B) - // 4 month (MISCALE 2 181B) - // 5 day (MISCALE 2 181B) - // 6 hour (MISCALE 2 181B) - // 7 minute (MISCALE 2 181B) - // 8 second (MISCALE 2 181B) - // 9-10 impedance (MISCALE 2 181B) - // 11-12 weight (MISCALE 2 181B) - - const uint8_t *data = message.data(); - const int data_length = 13; - - if (message.size() != data_length) { - ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); - return false; - } - - bool is_Stabilized = ((data[1] & (1 << 5)) != 0) ? true : false; - bool loadRemoved = ((data[1] & (1 << 7)) != 0) ? true : false; - - // weight, 2 bytes, 16-bit unsigned integer, 1 kg - const int16_t weight = uint16_t(data[11]) | (uint16_t(data[12]) << 8); - if (data[0] == 0x02) - result.weight = weight * 0.01f / 2.0f; // unit 'kg' - else if (data[0] == 0x03) - result.weight = weight * 0.01f * 0.453592; // unit 'lbs' - - // impedance, 2 bytes, 16-bit - const int16_t impedance = uint16_t(data[9]) | (uint16_t(data[10]) << 8); - result.impedance = impedance; - - if (!is_Stabilized || loadRemoved || impedance == 0 || impedance >= 3000) { - return false; - } - - return true; -} - -bool XiaomiMiscale2::report_results(const optional &result, const std::string &address) { - if (!result.has_value()) { - ESP_LOGVV(TAG, "report_results(): no results available."); - return false; - } - - ESP_LOGD(TAG, "Got Xiaomi Miscale2 (%s):", address.c_str()); - - if (result->weight.has_value()) { - ESP_LOGD(TAG, " Weight: %.2fkg", *result->weight); - } - if (result->impedance.has_value()) { - ESP_LOGD(TAG, " Impedance: %.0fohm", *result->impedance); - } - - return true; -} - -} // namespace xiaomi_miscale2 -} // namespace esphome - -#endif diff --git a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h deleted file mode 100644 index ead522e1f2..0000000000 --- a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" - -#ifdef ARDUINO_ARCH_ESP32 - -namespace esphome { -namespace xiaomi_miscale2 { - -struct ParseResult { - optional weight; - optional impedance; -}; - -class XiaomiMiscale2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { - public: - void set_address(uint64_t address) { 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_weight(sensor::Sensor *weight) { weight_ = weight; } - void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } - - protected: - uint64_t address_; - sensor::Sensor *weight_{nullptr}; - sensor::Sensor *impedance_{nullptr}; - - optional parse_header(const esp32_ble_tracker::ServiceData &service_data); - bool parse_message(const std::vector &message, ParseResult &result); - bool report_results(const optional &result, const std::string &address); -}; - -} // namespace xiaomi_miscale2 -} // namespace esphome - -#endif diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 90b971c08a..fd4bae60c1 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -9,9 +9,7 @@ from esphome.const import ( CONF_LIGHT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_PERCENT, @@ -43,21 +41,22 @@ CONFIG_SCHEMA = cv.All( CONF_DEVICE_CLASS, default="motion" ): binary_sensor.device_class, cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( - UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMELAPSE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend( { diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp index 739543ddb6..16c0b42279 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -1,7 +1,7 @@ #include "xiaomi_mjyd02yla.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mjyd02yla { @@ -57,11 +57,7 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) success = true; } - if (!success) { - return false; - } - - return true; + return success; } void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { @@ -72,7 +68,7 @@ void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { char temp[3] = {0}; for (int i = 0; i < 16; i++) { strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, NULL, 16); + bindkey_[i] = std::strtoul(temp, nullptr, 16); } } diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index 973b19a372..34b1fe4af0 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -6,7 +6,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mjyd02yla { diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp index c1eff2e066..1a8e72bd2c 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -1,7 +1,7 @@ #include "xiaomi_mue4094rt.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mue4094rt { @@ -46,11 +46,7 @@ bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_mue4094rt diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index 31f913ec94..904c575ae6 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -5,7 +5,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_mue4094rt { diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py index 90d4702da4..d2b353beff 100644 --- a/esphome/components/xiaomi_wx08zm/binary_sensor.py +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -6,8 +6,6 @@ from esphome.const import ( CONF_MAC_ADDRESS, CONF_TABLET, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_BUG, @@ -32,14 +30,16 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TABLET): sensor.sensor_schema( - UNIT_PERCENT, ICON_BUG, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BUG, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp index d6e2ebe5b2..b57bf5cd05 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -1,7 +1,7 @@ #include "xiaomi_wx08zm.h" #include "esphome/core/log.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_wx08zm { @@ -51,11 +51,7 @@ bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { success = true; } - if (!success) { - return false; - } - - return true; + return success; } } // namespace xiaomi_wx08zm diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index f3eba0e159..297c7ab47d 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -6,7 +6,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/xiaomi_ble/xiaomi_ble.h" -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 namespace esphome { namespace xiaomi_wx08zm { diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index 2615d7a353..8d588127b0 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -82,11 +82,14 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; climate::ClimateTraits YashimaClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); + + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); traits.set_visual_min_temperature(YASHIMA_TEMP_MIN); traits.set_visual_max_temperature(YASHIMA_TEMP_MAX); traits.set_visual_temperature_step(1); @@ -139,7 +142,7 @@ void YashimaClimate::transmit_state_() { // Set mode switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0; remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5; break; diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index 5f9a5e3add..28a708b866 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -9,10 +9,9 @@ from esphome.const import ( CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, @@ -27,28 +26,26 @@ ZyAuraSensor = zyaura_ns.class_("ZyAuraSensor", cg.PollingComponent) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ZyAuraSensor), - cv.Required(CONF_CLOCK_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), - cv.Required(CONF_DATA_PIN): cv.All( - pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt - ), + cv.Required(CONF_CLOCK_PIN): cv.All(pins.internal_gpio_input_pin_schema), + cv.Required(CONF_DATA_PIN): cv.All(pins.internal_gpio_input_pin_schema), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp index 989791f098..11643a5c23 100644 --- a/esphome/components/zyaura/zyaura.cpp +++ b/esphome/components/zyaura/zyaura.cpp @@ -6,7 +6,7 @@ namespace zyaura { static const char *const TAG = "zyaura"; -bool ICACHE_RAM_ATTR ZaDataProcessor::decode(uint32_t ms, bool data) { +bool IRAM_ATTR ZaDataProcessor::decode(uint32_t ms, bool data) { // check if a new message has started, based on time since previous bit if ((ms - this->prev_ms_) > ZA_MAX_MS) { this->num_bits_ = 0; @@ -37,24 +37,24 @@ bool ICACHE_RAM_ATTR ZaDataProcessor::decode(uint32_t ms, bool data) { return false; } -void ZaSensorStore::setup(GPIOPin *pin_clock, GPIOPin *pin_data) { +void ZaSensorStore::setup(InternalGPIOPin *pin_clock, InternalGPIOPin *pin_data) { pin_clock->setup(); pin_data->setup(); this->pin_clock_ = pin_clock->to_isr(); this->pin_data_ = pin_data->to_isr(); - pin_clock->attach_interrupt(ZaSensorStore::interrupt, this, FALLING); + pin_clock->attach_interrupt(ZaSensorStore::interrupt, this, gpio::INTERRUPT_FALLING_EDGE); } -void ICACHE_RAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) { +void IRAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) { uint32_t now = millis(); - bool data_bit = arg->pin_data_->digital_read(); + bool data_bit = arg->pin_data_.digital_read(); if (arg->processor_.decode(now, data_bit)) { arg->set_data_(arg->processor_.message); } } -void ICACHE_RAM_ATTR ZaSensorStore::set_data_(ZaMessage *message) { +void IRAM_ATTR ZaSensorStore::set_data_(ZaMessage *message) { switch (message->type) { case HUMIDITY: this->humidity = (message->value > 10000) ? NAN : (message->value / 100.0f); @@ -82,7 +82,7 @@ bool ZyAuraSensor::publish_state_(sensor::Sensor *sensor, float *value) { sensor->publish_state(*value); // Sensor reported wrong value - if (isnan(*value)) { + if (std::isnan(*value)) { ESP_LOGW(TAG, "Sensor reported invalid data. Is the update interval too small?"); this->status_set_warning(); return false; diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index eed0c55c35..2b9e3fbb35 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -46,12 +46,12 @@ class ZaSensorStore { float temperature = NAN; float humidity = NAN; - void setup(GPIOPin *pin_clock, GPIOPin *pin_data); + void setup(InternalGPIOPin *pin_clock, InternalGPIOPin *pin_data); static void interrupt(ZaSensorStore *arg); protected: - ISRInternalGPIOPin *pin_clock_; - ISRInternalGPIOPin *pin_data_; + ISRInternalGPIOPin pin_clock_; + ISRInternalGPIOPin pin_data_; ZaDataProcessor processor_; void set_data_(ZaMessage *message); @@ -60,8 +60,8 @@ class ZaSensorStore { /// Component for reading temperature/co2/humidity measurements from ZyAura sensors. class ZyAuraSensor : public PollingComponent { public: - void set_pin_clock(GPIOPin *pin) { pin_clock_ = pin; } - void set_pin_data(GPIOPin *pin) { pin_data_ = pin; } + void set_pin_clock(InternalGPIOPin *pin) { pin_clock_ = pin; } + void set_pin_data(InternalGPIOPin *pin) { pin_data_ = pin; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -73,8 +73,8 @@ class ZyAuraSensor : public PollingComponent { protected: ZaSensorStore store_; - GPIOPin *pin_clock_; - GPIOPin *pin_data_; + InternalGPIOPin *pin_clock_; + InternalGPIOPin *pin_data_; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/config.py b/esphome/config.py index fcd2fac90f..af6c5b0b64 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,4 +1,6 @@ -import collections +import abc +import functools +import heapq import logging import re @@ -15,17 +17,20 @@ from esphome.const import ( CONF_PACKAGES, CONF_SUBSTITUTIONS, CONF_EXTERNAL_COMPONENTS, + TARGET_PLATFORMS, ) from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union -from esphome.core import ConfigType 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 +import esphome.final_validate as fv +import esphome.config_validation as cv +from esphome.types import ConfigType, ConfigPathType, ConfigFragmentType _LOGGER = logging.getLogger(__name__) @@ -40,7 +45,7 @@ def iter_components(config): yield domain, component, conf if component.is_platform_component: for p_config in conf: - p_name = "{}.{}".format(domain, p_config[CONF_PLATFORM]) + p_name = f"{domain}.{p_config[CONF_PLATFORM]}" platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config @@ -54,7 +59,28 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool return path[: len(other)] == other -class Config(OrderedDict): +@functools.total_ordering +class _ValidationStepTask: + def __init__(self, priority: float, id_number: int, step: "ConfigValidationStep"): + self.priority = priority + self.id_number = id_number + self.step = step + + @property + def _cmp_tuple(self) -> Tuple[float, int]: + return (-self.priority, self.id_number) + + def __eq__(self, other): + return self._cmp_tuple == other._cmp_tuple + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return self._cmp_tuple < other._cmp_tuple + + +class Config(OrderedDict, fv.FinalValidateConfig): def __init__(self): super().__init__() # A list of voluptuous errors @@ -65,6 +91,11 @@ class Config(OrderedDict): self.output_paths = [] # type: List[Tuple[ConfigPath, str]] # A list of components ids with the config path self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + self._data = {} + # Store pending validation tasks (in heap order) + self._validation_tasks: List[_ValidationStepTask] = [] + # ID to ensure stable order for keys with equal priority + self._validation_tasks_id = 0 def add_error(self, error): # type: (vol.Invalid) -> None @@ -72,8 +103,26 @@ class Config(OrderedDict): for err in error.errors: self.add_error(err) return + if cv.ROOT_CONFIG_PATH in error.path: + # Root value means that the path before the root should be ignored + 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 :] self.errors.append(error) + def add_validation_step(self, step: "ConfigValidationStep"): + id_num = self._validation_tasks_id + self._validation_tasks_id += 1 + heapq.heappush( + self._validation_tasks, _ValidationStepTask(step.priority, id_num, step) + ) + + def run_validation_steps(self): + while self._validation_tasks: + task = heapq.heappop(self._validation_tasks) + task.step.run(self) + @contextmanager def catch_error(self, path=None): path = path or [] @@ -140,13 +189,16 @@ class Config(OrderedDict): return doc_range - def get_nested_item(self, path): - # type: (ConfigPath) -> ConfigType + def get_nested_item( + self, path: ConfigPathType, raise_error: bool = False + ) -> ConfigFragmentType: data = self for item_index in path: try: data = data[item_index] except (KeyError, IndexError, TypeError): + if raise_error: + raise return {} return data @@ -163,11 +215,20 @@ class Config(OrderedDict): part.append(item_index) return part - def get_config_by_id(self, id): + def get_path_for_id(self, id: core.ID): + """Return the config fragment where the given ID is declared.""" for declared_id, path in self.declare_ids: if declared_id.id == str(id): - return self.get_nested_item(path[:-1]) - return None + return path + raise KeyError(f"ID {id} not found in configuration") + + def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType: + return self.get_nested_item(path, raise_error=True) + + @property + def data(self): + """Return temporary data used by final validation functions.""" + return self._data def iter_ids(config, path=None): @@ -185,101 +246,7 @@ def iter_ids(config, path=None): yield from iter_ids(value, path + [key]) -def do_id_pass(result): # type: (Config) -> None - from esphome.cpp_generator import MockObjClass - from esphome.cpp_types import Component - - declare_ids = result.declare_ids # type: List[Tuple[core.ID, ConfigPath]] - searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] - for id, path in iter_ids(result): - if id.is_declaration: - if id.id is not None: - # Look for duplicate definitions - match = next((v for v in declare_ids if v[0].id == id.id), None) - if match is not None: - opath = "->".join(str(v) for v in match[1]) - result.add_str_error(f"ID {id.id} redefined! Check {opath}", path) - continue - declare_ids.append((id, path)) - else: - searching_ids.append((id, path)) - # Resolve default ids after manual IDs - for id, _ in declare_ids: - id.resolve([v[0].id for v in declare_ids]) - if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component): - CORE.component_ids.add(id.id) - - # Check searched IDs - for id, path in searching_ids: - if id.id is not None: - # manually declared - match = next((v[0] for v in declare_ids if v[0].id == id.id), None) - if match is None or not match.is_manual: - # No declared ID with this name - import difflib - - error = ( - "Couldn't find ID '{}'. Please check you have defined " - "an ID with that name in your configuration.".format(id.id) - ) - # Find candidates - matches = difflib.get_close_matches( - id.id, [v[0].id for v in declare_ids if v[0].is_manual] - ) - if matches: - matches_s = ", ".join(f'"{x}"' for x in matches) - error += f" These IDs look similar: {matches_s}." - result.add_str_error(error, path) - continue - if not isinstance(match.type, MockObjClass) or not isinstance( - id.type, MockObjClass - ): - continue - if not match.type.inherits_from(id.type): - result.add_str_error( - "ID '{}' of type {} doesn't inherit from {}. Please " - "double check your ID is pointing to the correct value" - "".format(id.id, match.type, id.type), - path, - ) - - if id.id is None and id.type is not None: - matches = [] - for v in declare_ids: - if v[0] is None or not isinstance(v[0].type, MockObjClass): - continue - inherits = v[0].type.inherits_from(id.type) - if inherits: - matches.append(v[0]) - - if len(matches) == 0: - result.add_str_error( - f"Couldn't find any component that can be used for '{id.type}'. Are you missing a hub declaration?", - path, - ) - elif len(matches) == 1: - id.id = matches[0].id - elif len(matches) > 1: - if str(id.type) == "time::RealTimeClock": - id.id = matches[0].id - else: - manual_declared_count = sum(1 for m in matches if m.is_manual) - if manual_declared_count > 0: - ids = ", ".join([f"'{m.id}'" for m in matches if m.is_manual]) - result.add_str_error( - f"Too many candidates found for '{path[-1]}' type '{id.type}' {'Some are' if manual_declared_count > 1 else 'One is'} {ids}", - path, - ) - else: - result.add_str_error( - f"Too many candidates found for '{path[-1]}' type '{id.type}' You must assign an explicit ID to the parent component you want to use.", - path, - ) - - def recursive_check_replaceme(value): - import esphome.config_validation as cv - if isinstance(value, list): return cv.Schema([recursive_check_replaceme])(value) if isinstance(value, dict): @@ -297,6 +264,389 @@ def recursive_check_replaceme(value): return value +class ConfigValidationStep(abc.ABC): + """A step to for the validation phase.""" + + # Priority of this step, higher means run earlier + priority: float = 0.0 + + @abc.abstractmethod + def run(self, result: Config) -> None: + ... + + +class LoadValidationStep(ConfigValidationStep): + """Load step, this step is called once for each domain config fragment. + + Responsibilties: + - Load component code + - Ensure all AUTO_LOADs are added + - Set output paths of result + """ + + def __init__(self, domain: str, conf: ConfigType): + self.domain = domain + self.conf = conf + + def run(self, result: Config) -> None: + if self.domain.startswith("."): + # 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) + path = [self.domain] + if component is None: + result.add_str_error(f"Component not found: {self.domain}", path) + return + CORE.loaded_integrations.add(self.domain) + + # Process AUTO_LOAD + for load in component.auto_load: + if load not in result: + result.add_validation_step(AutoLoadValidationStep(load)) + + 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 + # Remove this is as an output path + result.remove_output_path([self.domain], self.domain) + + # Ensure conf is a list + if not self.conf: + result[self.domain] = self.conf = [] + elif not isinstance(self.conf, list): + result[self.domain] = self.conf = [self.conf] + + for i, p_config in enumerate(self.conf): + path = [self.domain, i] + # Construct temporary unknown output path + p_domain = f"{self.domain}.unknown" + result.add_output_path(path, p_domain) + result[self.domain][i] = p_config + if not isinstance(p_config, dict): + result.add_str_error("Platform schemas must be key-value pairs.", path) + continue + p_name = p_config.get("platform") + if p_name is None: + result.add_str_error("No platform specified! See 'platform' key.", path) + continue + # Remove temp output path and construct new one + result.remove_output_path(path, p_domain) + p_domain = f"{self.domain}.{p_name}" + result.add_output_path(path, p_domain) + # Try Load platform + platform = get_platform(self.domain, p_name) + if platform is None: + result.add_str_error(f"Platform not found: '{p_domain}'", path) + continue + CORE.loaded_integrations.add(p_name) + + # Process AUTO_LOAD + for load in platform.auto_load: + if load not in result: + result.add_validation_step(AutoLoadValidationStep(load)) + + result.add_validation_step( + MetadataValidationStep(path, p_domain, p_config, platform) + ) + + +class AutoLoadValidationStep(ConfigValidationStep): + """Auto load step. This step is used to automatically load components if + a component requested that with AUTO_LOAD. + """ + + # Only load after all regular loads have taken place + priority = -1.0 + + def __init__(self, domain: str): + self.domain = domain + + def run(self, result: Config) -> None: + if self.domain in result: + # already loaded + return + result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad())) + + +class MetadataValidationStep(ConfigValidationStep): + """Validate component metadata + + Responsibilties: + - Config transformation (nullable, multi conf) + - Check dependencies + - Check conflicts + - Check supported target platforms + """ + + # All components need to be loaded first to ensure dependency check works + priority = -2.0 + + def __init__( + self, + path: ConfigPath, + domain: str, + conf: ConfigType, + component: ComponentManifest, + ) -> None: + self.path = path + self.domain = domain + self.conf = conf + self.comp = component + + def run(self, result: Config) -> None: + if self.conf is None: + result[self.domain] = self.conf = {} + + success = True + for dependency in self.comp.dependencies: + if dependency not in result: + result.add_str_error( + f"Component {self.domain} requires component {dependency}", + self.path, + ) + success = False + if not success: + return + + success = True + for conflict in self.comp.conflicts_with: + if conflict in result: + result.add_str_error( + f"Component {self.domain} cannot be used together with component {conflict}", + self.path, + ) + success = False + if not success: + return + + if ( + not self.comp.is_platform_component + and self.comp.config_schema is None + and not isinstance(self.conf, core.AutoLoad) + ): + result.add_str_error( + f"Component {self.domain} cannot be loaded via YAML " + "(no CONFIG_SCHEMA).", + self.path, + ) + return + + if self.comp.multi_conf: + if not isinstance(self.conf, list): + result[self.domain] = self.conf = [self.conf] + if ( + not isinstance(self.comp.multi_conf, bool) + and len(self.conf) > self.comp.multi_conf + ): + result.add_str_error( + f"Component {self.domain} supports a maximum of {self.comp.multi_conf} " + f"entries ({len(self.conf)} found).", + self.path, + ) + return + for i, part_conf in enumerate(self.conf): + result.add_validation_step( + SchemaValidationStep( + self.domain, self.path + [i], part_conf, self.comp + ) + ) + return + + result.add_validation_step( + SchemaValidationStep(self.domain, self.path, self.conf, self.comp) + ) + + +class SchemaValidationStep(ConfigValidationStep): + """Schema validation step. + + During this step all CONFIG_SCHEMAs are checked against the configs. + """ + + def __init__( + self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest + ): + self.path = path + self.conf = conf + self.comp = comp + + def run(self, result: Config) -> None: + if self.comp.config_schema is None: + return + with result.catch_error(self.path): + if self.comp.is_platform: + # Remove 'platform' key for validation + input_conf = OrderedDict(self.conf) + platform_val = input_conf.pop("platform") + schema = cv.Schema(self.comp.config_schema) + validated = schema(input_conf) + # Ensure result is OrderedDict so we can call move_to_end + if not isinstance(validated, OrderedDict): + validated = OrderedDict(validated) + validated["platform"] = platform_val + validated.move_to_end("platform", last=False) + result.set_by_path(self.path, validated) + else: + schema = cv.Schema(self.comp.config_schema) + validated = schema(self.conf) + result.set_by_path(self.path, validated) + + result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) + + +class IDPassValidationStep(ConfigValidationStep): + """ID Pass step. + + During this step all ID references are checked. + + If an automatic ID reference is used, a fitting declared ID is automatically searched. + Also checks duplicate ID names, and that referenced IDs are declared. + """ + + # Has to happen after all schemas validated + priority = -10.0 + + def __init__(self) -> None: + pass + + def run(self, result: Config) -> None: + from esphome.cpp_generator import MockObjClass + from esphome.cpp_types import Component + + if result.errors: + # If result already has errors, skip this step + # Otherwise the user will get a bunch of missing ID warnings + # because the component that did not validate doesn't have any IDs set + return + + searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + for id, path in iter_ids(result): + if id.is_declaration: + if id.id is not None: + # Look for duplicate definitions + match = next( + (v for v in result.declare_ids if v[0].id == id.id), None + ) + if match is not None: + opath = "->".join(str(v) for v in match[1]) + result.add_str_error( + f"ID {id.id} redefined! Check {opath}", path + ) + continue + result.declare_ids.append((id, path)) + else: + searching_ids.append((id, path)) + + # Resolve default ids after manual IDs + for id, _ in result.declare_ids: + id.resolve([v[0].id for v in result.declare_ids]) + if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component): + CORE.component_ids.add(id.id) + + # Check searched IDs + for id, path in searching_ids: + if id.id is not None: + # manually declared + match = next( + (v[0] for v in result.declare_ids if v[0].id == id.id), None + ) + if match is None or not match.is_manual: + # No declared ID with this name + import difflib + + error = ( + f"Couldn't find ID '{id.id}'. Please check you have defined " + "an ID with that name in your configuration." + ) + # Find candidates + matches = difflib.get_close_matches( + id.id, [v[0].id for v in result.declare_ids if v[0].is_manual] + ) + if matches: + matches_s = ", ".join(f'"{x}"' for x in matches) + error += f" These IDs look similar: {matches_s}." + result.add_str_error(error, path) + continue + if not isinstance(match.type, MockObjClass) or not isinstance( + id.type, MockObjClass + ): + continue + if not match.type.inherits_from(id.type): + result.add_str_error( + f"ID '{id.id}' of type {match.type} doesn't inherit from {id.type}. " + "Please double check your ID is pointing to the correct value", + path, + ) + + if id.id is None and id.type is not None: + matches = [] + for v in result.declare_ids: + if v[0] is None or not isinstance(v[0].type, MockObjClass): + continue + inherits = v[0].type.inherits_from(id.type) + if inherits: + matches.append(v[0]) + + if len(matches) == 0: + result.add_str_error( + f"Couldn't find any component that can be used for '{id.type}'. Are you missing a hub declaration?", + path, + ) + elif len(matches) == 1: + id.id = matches[0].id + elif len(matches) > 1: + if str(id.type) == "time::RealTimeClock": + id.id = matches[0].id + else: + manual_declared_count = sum(1 for m in matches if m.is_manual) + if manual_declared_count > 0: + ids = ", ".join( + [f"'{m.id}'" for m in matches if m.is_manual] + ) + result.add_str_error( + f"Too many candidates found for '{path[-1]}' type '{id.type}' {'Some are' if manual_declared_count > 1 else 'One is'} {ids}", + path, + ) + else: + result.add_str_error( + f"Too many candidates found for '{path[-1]}' type '{id.type}' You must assign an explicit ID to the parent component you want to use.", + path, + ) + + +class FinalValidateValidationStep(ConfigValidationStep): + """Run final_validate_schema for all components.""" + + # Has to happen after ID pass validated + priority = -20.0 + + def __init__(self, path: ConfigPath, comp: ComponentManifest) -> None: + self.path = path + self.comp = comp + + def run(self, result: Config) -> None: + if result.errors: + # 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) + + fv.full_config.reset(token) + + def validate_config(config, command_line_substitutions): result = Config() @@ -315,6 +665,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: from esphome.components import substitutions @@ -330,6 +682,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1.1. Check for REPLACEME special value try: recursive_check_replaceme(config) @@ -367,218 +721,31 @@ def validate_config(config, command_line_substitutions): result[CONF_ESPHOME] = config[CONF_ESPHOME] result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) try: - core_config.preload_core_config(config) + core_config.preload_core_config(config, result) except vol.Invalid as err: result.add_error(err) return result # Remove temporary esphome config path again, it will be reloaded later result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME) - # 3. Load components. - # Load components (also AUTO_LOAD) and set output paths of result - # Queue of items to load, FIFO - load_queue = collections.deque() + # First run platform validation steps + for key in TARGET_PLATFORMS: + if key in config: + result.add_validation_step(LoadValidationStep(key, config[key])) + result.run_validation_steps() + + if result.errors: + return result + for domain, conf in config.items(): - load_queue.append((domain, conf)) + result.add_validation_step(LoadValidationStep(domain, conf)) + result.add_validation_step(IDPassValidationStep()) - # List of items to enter next stage - check_queue = ( - [] - ) # type: List[Tuple[ConfigPath, str, ConfigType, ComponentManifest]] - - # This step handles: - # - Adding output path - # - Auto Load - # - Loading configs into result - - while load_queue: - domain, conf = load_queue.popleft() - if domain.startswith("."): - # Ignore top-level keys starting with a dot - continue - result.add_output_path([domain], domain) - result[domain] = conf - component = get_component(domain) - path = [domain] - if component is None: - result.add_str_error(f"Component not found: {domain}", path) - continue - CORE.loaded_integrations.add(domain) - - # Process AUTO_LOAD - for load in component.auto_load: - if load not in config: - load_conf = core.AutoLoad() - config[load] = load_conf - load_queue.append((load, load_conf)) - - if not component.is_platform_component: - check_queue.append(([domain], domain, conf, component)) - continue - - # This is a platform component, proceed to reading platform entries - # Remove this is as an output path - result.remove_output_path([domain], domain) - - # Ensure conf is a list - if not conf: - result[domain] = conf = [] - elif not isinstance(conf, list): - result[domain] = conf = [conf] - - for i, p_config in enumerate(conf): - path = [domain, i] - # Construct temporary unknown output path - p_domain = f"{domain}.unknown" - result.add_output_path(path, p_domain) - result[domain][i] = p_config - if not isinstance(p_config, dict): - result.add_str_error("Platform schemas must be key-value pairs.", path) - continue - p_name = p_config.get("platform") - if p_name is None: - result.add_str_error("No platform specified! See 'platform' key.", path) - continue - # Remove temp output path and construct new one - result.remove_output_path(path, p_domain) - p_domain = f"{domain}.{p_name}" - result.add_output_path(path, p_domain) - # Try Load platform - platform = get_platform(domain, p_name) - if platform is None: - result.add_str_error(f"Platform not found: '{p_domain}'", path) - continue - CORE.loaded_integrations.add(p_name) - - # Process AUTO_LOAD - for load in platform.auto_load: - if load not in config: - load_conf = core.AutoLoad() - config[load] = load_conf - load_queue.append((load, load_conf)) - - check_queue.append((path, p_domain, p_config, platform)) - - # 4. Validate component metadata, including - # - Transformation (nullable, multi conf) - # - Dependencies - # - Conflicts - # - Supported ESP Platform - - # List of items to proceed to next stage - validate_queue = [] # type: List[Tuple[ConfigPath, ConfigType, ComponentManifest]] - for path, domain, conf, comp in check_queue: - if conf is None: - result[domain] = conf = {} - - success = True - for dependency in comp.dependencies: - if dependency not in config: - result.add_str_error( - "Component {} requires component {}" "".format(domain, dependency), - path, - ) - success = False - if not success: - continue - - success = True - for conflict in comp.conflicts_with: - if conflict in config: - result.add_str_error( - "Component {} cannot be used together with component {}" - "".format(domain, conflict), - path, - ) - success = False - if not success: - continue - - if CORE.esp_platform not in comp.esp_platforms: - result.add_str_error( - "Component {} doesn't support {}.".format(domain, CORE.esp_platform), - path, - ) - continue - - if ( - not comp.is_platform_component - and comp.config_schema is None - and not isinstance(conf, core.AutoLoad) - ): - result.add_str_error( - "Component {} cannot be loaded via YAML " - "(no CONFIG_SCHEMA).".format(domain), - path, - ) - continue - - if comp.multi_conf: - if not isinstance(conf, list): - result[domain] = conf = [conf] - if not isinstance(comp.multi_conf, bool) and len(conf) > comp.multi_conf: - result.add_str_error( - "Component {} supports a maximum of {} " - "entries ({} found).".format(domain, comp.multi_conf, len(conf)), - path, - ) - continue - for i, part_conf in enumerate(conf): - validate_queue.append((path + [i], part_conf, comp)) - continue - - validate_queue.append((path, conf, comp)) - - # 5. Validate configuration schema - for path, conf, comp in validate_queue: - if comp.config_schema is None: - continue - with result.catch_error(path): - if comp.is_platform: - # Remove 'platform' key for validation - input_conf = OrderedDict(conf) - platform_val = input_conf.pop("platform") - validated = comp.config_schema(input_conf) - # Ensure result is OrderedDict so we can call move_to_end - if not isinstance(validated, OrderedDict): - validated = OrderedDict(validated) - validated["platform"] = platform_val - validated.move_to_end("platform", last=False) - result.set_by_path(path, validated) - else: - validated = comp.config_schema(conf) - result.set_by_path(path, validated) - - # 6. If no validation errors, check IDs - if not result.errors: - # Only parse IDs if no validation error. Otherwise - # user gets confusing messages - do_id_pass(result) - - # 7. Final validation - if not result.errors: - # Inter - components validation - for path, conf, comp in validate_queue: - if comp.config_schema is None: - continue - if callable(comp.validate): - try: - comp.validate(result, result.get_nested_item(path)) - except ValueError as err: - result.add_str_error(err, path) + result.run_validation_steps() return result -def _nested_getitem(data, path): - for item_index in path: - try: - data = data[item_index] - except (KeyError, IndexError, TypeError): - return None - return data - - def humanize_error(config, validation_error): validation_error = str(validation_error) m = re.match( @@ -612,17 +779,17 @@ def _format_vol_invalid(ex, config): if isinstance(ex, ExtraKeysInvalid): if ex.candidates: - message += "[{}] is an invalid option for [{}]. Did you mean {}?".format( - ex.path[-1], paren, ", ".join(f"[{x}]" for x in ex.candidates) - ) + message += f"[{ex.path[-1]}] is an invalid option for [{paren}]. Did you mean {', '.join(f'[{x}]' for x in ex.candidates)}?" else: - message += "[{}] is an invalid option for [{}]. Please check the indentation.".format( - ex.path[-1], paren - ) + message += f"[{ex.path[-1]}] is an invalid option for [{paren}]. Please check the indentation." elif "extra keys not allowed" in str(ex): - message += "[{}] is an invalid option for [{}].".format(ex.path[-1], paren) - elif "required key not provided" in str(ex): - message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) + message += f"[{ex.path[-1]}] is an invalid option for [{paren}]." + elif isinstance(ex, vol.RequiredFieldInvalid): + if ex.msg == "required key not provided": + message += f"'{ex.path[-1]}' is a required option for [{paren}]." + else: + # Required has set a custom error message + message += ex.msg else: message += humanize_error(config, ex) @@ -645,7 +812,6 @@ def _load_config(command_line_substitutions): config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: raise InvalidYAMLError(e) from e - CORE.raw_config = config try: result = validate_config(config, command_line_substitutions) @@ -672,7 +838,7 @@ def line_info(config, path, highlight=True): obj = config.get_deepest_document_range_for_path(path) if obj: mark = obj.start_mark - source = "[source {}:{}]".format(mark.document, mark.line + 1) + source = f"[source {mark.document}:{mark.line + 1}]" return color(Fore.CYAN, source) return "None" @@ -696,9 +862,7 @@ def dump_dict(config, path, at_root=True): if at_root: error = config.get_error_for_path(path) if error is not None: - ret += ( - "\n" + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) + "\n" - ) + ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" if isinstance(conf, (list, tuple)): multiline = True @@ -710,11 +874,7 @@ def dump_dict(config, path, at_root=True): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += ( - "\n" - + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) - + "\n" - ) + ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" sep = "- " if config.is_in_error_path(path_): @@ -723,10 +883,10 @@ def dump_dict(config, path, at_root=True): msg = indent(msg) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) if inf is not None: - msg = inf + "\n" + msg + msg = f"{inf}\n{msg}" elif msg: msg = msg[2:] - ret += sep + msg + "\n" + ret += f"{sep + msg}\n" elif isinstance(conf, dict): multiline = True if not conf: @@ -737,11 +897,7 @@ def dump_dict(config, path, at_root=True): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += ( - "\n" - + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) - + "\n" - ) + ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n" st = f"{k}: " if config.is_in_error_path(path_): @@ -750,30 +906,30 @@ def dump_dict(config, path, at_root=True): inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) if m: - msg = "\n" + indent(msg) + msg = f"\n{indent(msg)}" if inf is not None: if m: - msg = " " + inf + msg + msg = f" {inf}{msg}" else: - msg = msg + " " + inf - ret += st + msg + "\n" + msg = f"{msg} {inf}" + ret += f"{st + msg}\n" elif isinstance(conf, str): if is_secret(conf): - conf = "!secret {}".format(is_secret(conf)) + conf = f"!secret {is_secret(conf)}" if not conf: conf += "''" if len(conf) > 80: - conf = "|-\n" + indent(conf) + conf = f"|-\n{indent(conf)}" error = config.get_error_for_path(path) col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): - conf = "!secret {}".format(is_secret(conf)) + conf = f"!secret {is_secret(conf)}" - conf = "!lambda |-\n" + indent(str(conf.value)) + conf = f"!lambda |-\n{indent(str(conf.value))}" error = config.get_error_for_path(path) col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, conf) @@ -832,7 +988,7 @@ def read_config(command_line_substitutions): errstr = color(Fore.BOLD_RED, f"{domain}:") errline = line_info(res, path) if errline: - errstr += " " + errline + errstr += f" {errline}" safe_print(errstr) safe_print(indent(dump_dict(res, path)[0])) return None diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2cdb6b0b76..fcec74b245 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,5 +1,6 @@ """Helpers for config validation using voluptuous.""" +from dataclasses import dataclass import logging import os import re @@ -15,7 +16,9 @@ from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TOPIC, + CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, + CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_NAME, @@ -32,7 +35,9 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE, - CONF_PACKAGES, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, ) from esphome.core import ( CORE, @@ -75,6 +80,9 @@ Inclusive = vol.Inclusive ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid +# this sentinel object can be placed in an 'Invalid' path to say +# the rest of the error path is relative to the root config path +ROOT_CONFIG_PATH = object() RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword @@ -218,8 +226,8 @@ class Required(vol.Required): - *not* the `config.get(CONF_)` syntax. """ - def __init__(self, key): - super().__init__(key) + def __init__(self, key, msg=None): + super().__init__(key, msg=msg) def check_not_templatable(value): @@ -274,8 +282,7 @@ def string_strict(value): if isinstance(value, str): return value raise Invalid( - "Must be string, got {}. did you forget putting quotes " - "around the value?".format(type(value)) + f"Must be string, got {type(value)}. did you forget putting quotes around the value?" ) @@ -308,8 +315,7 @@ def boolean(value): if value in ("false", "no", "off", "disable"): return False raise Invalid( - "Expected boolean value, but cannot convert {} to a boolean. " - "Please use 'true' or 'false'".format(value) + f"Expected boolean value, but cannot convert {value} to a boolean. Please use 'true' or 'false'" ) @@ -355,8 +361,7 @@ def int_(value): if int(value) == value: return int(value) raise Invalid( - "This option only accepts integers with no fractional part. Please remove " - "the fractional part from {}".format(value) + f"This option only accepts integers with no fractional part. Please remove the fractional part from {value}" ) value = string_strict(value).lower() base = 10 @@ -421,20 +426,17 @@ def validate_id_name(value): raise Invalid( "Dashes are not supported in IDs, please use underscores instead." ) - valid_chars = ascii_letters + digits + "_" + valid_chars = f"{ascii_letters + digits}_" for char in value: if char not in valid_chars: raise Invalid( - "IDs must only consist of upper/lowercase characters, the underscore" - "character and numbers. The character '{}' cannot be used" - "".format(char) + f"IDs must only consist of upper/lowercase characters, the underscorecharacter and numbers. The character '{char}' cannot be used" ) if value in RESERVED_IDS: raise Invalid(f"ID '{value}' is reserved internally and cannot be used") if value in CORE.loaded_integrations: raise Invalid( - "ID '{}' conflicts with the name of an esphome integration, please use " - "another ID name.".format(value) + f"ID '{value}' conflicts with the name of an esphome integration, please use another ID name." ) return value @@ -495,20 +497,37 @@ def templatable(other_validators): def only_on(platforms): - """Validate that this option can only be specified on the given ESP platforms.""" + """Validate that this option can only be specified on the given target platforms.""" if not isinstance(platforms, list): platforms = [platforms] def validator_(obj): - if CORE.esp_platform not in platforms: + if CORE.target_platform not in platforms: raise Invalid(f"This feature is only available on {platforms}") return obj return validator_ -only_on_esp32 = only_on("ESP32") -only_on_esp8266 = only_on("ESP8266") +def only_with_framework(frameworks): + """Validate that this option can only be specified on the given frameworks.""" + if not isinstance(frameworks, list): + frameworks = [frameworks] + + def validator_(obj): + if CORE.target_framework not in frameworks: + raise Invalid( + f"This feature is only available with frameworks {frameworks}" + ) + return obj + + return validator_ + + +only_on_esp32 = only_on("esp32") +only_on_esp8266 = only_on("esp8266") +only_with_arduino = only_with_framework("arduino") +only_with_esp_idf = only_with_framework("esp-idf") # Adapted from: @@ -522,7 +541,7 @@ def has_at_least_one_key(*keys): raise Invalid("expected dictionary") if not any(k in keys for k in obj): - raise Invalid("Must contain at least one of {}.".format(", ".join(keys))) + raise Invalid(f"Must contain at least one of {', '.join(keys)}.") return obj return validate @@ -537,9 +556,9 @@ def has_exactly_one_key(*keys): number = sum(k in keys for k in obj) if number > 1: - raise Invalid("Cannot specify more than one of {}.".format(", ".join(keys))) + raise Invalid(f"Cannot specify more than one of {', '.join(keys)}.") if number < 1: - raise Invalid("Must contain exactly one of {}.".format(", ".join(keys))) + raise Invalid(f"Must contain exactly one of {', '.join(keys)}.") return obj return validate @@ -554,7 +573,22 @@ def has_at_most_one_key(*keys): number = sum(k in keys for k in obj) if number > 1: - raise Invalid("Cannot specify more than one of {}.".format(", ".join(keys))) + raise Invalid(f"Cannot specify more than one of {', '.join(keys)}.") + return obj + + return validate + + +def has_none_or_all_keys(*keys): + """Validate that none or all of the given keys exist in the config.""" + + def validate(obj): + if not isinstance(obj, dict): + raise Invalid("expected dictionary") + + number = sum(k in keys for k in obj) + if number != 0 and number != len(keys): + raise Invalid(f"Must specify either none or all of {', '.join(keys)}.") return obj return validate @@ -612,8 +646,7 @@ def time_period_str_unit(value): if isinstance(value, int): raise Invalid( - "Don't know what '{0}' means as it has no time *unit*! Did you mean " - "'{0}s'?".format(value) + f"Don't know what '{value}' means as it has no time *unit*! Did you mean '{value}s'?" ) if isinstance(value, TimePeriod): value = str(value) @@ -639,7 +672,7 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise Invalid("Expected time period with unit, " "got {}".format(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))}) @@ -776,7 +809,7 @@ METRIC_SUFFIXES = { def float_with_unit(quantity, regex_suffix, optional_unit=False): pattern = re.compile( - r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + r"$", re.UNICODE + f"^([-+]?[0-9]*\\.?[0-9]*)\\s*(\\w*?){regex_suffix}$", re.UNICODE ) def validator(value): @@ -792,7 +825,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid("Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid(f"Invalid {quantity} suffix {match.group(2)}") multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -815,10 +848,11 @@ pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) def temperature(value): + err = None try: return _temperature_c(value) - except Invalid as orig_err: # noqa - pass + except Invalid as orig_err: + err = orig_err try: kelvin = _temperature_k(value) @@ -832,7 +866,7 @@ def temperature(value): except Invalid: pass - raise orig_err # noqa + raise err _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)") @@ -858,23 +892,31 @@ def validate_bytes(value): mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid("Invalid metric suffix {}".format(match.group(2))) + raise Invalid(f"Invalid metric suffix {match.group(2)}") multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: raise Invalid( - "Only suffixes with positive exponents are supported. " - "Got {}".format(match.group(2)) + f"Only suffixes with positive exponents are supported. Got {match.group(2)}" ) return int(mantissa * multiplier) def hostname(value): value = string(value) + warned_underscore = False if len(value) > 63: raise Invalid("Hostnames can only be 63 characters long") for c in value: - if not (c.isalnum() or c in "_-"): - raise Invalid("Hostname can only have alphanumeric characters and _ or -") + if not (c.isalnum() or c in "-_"): + raise Invalid("Hostname can only have alphanumeric characters and -") + if c in "_" and not warned_underscore: + _LOGGER.warning( + "'%s': Using the '_' (underscore) character in the hostname is discouraged " + "as it can cause problems with some DHCP and local name services. " + "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name", + value, + ) + warned_underscore = True return value @@ -1005,7 +1047,7 @@ def requires_component(comp): # pylint: disable=unsupported-membership-test def validator(value): # pylint: disable=unsupported-membership-test - if comp not in CORE.raw_config: + if comp not in CORE.loaded_integrations: raise Invalid(f"This option requires component {comp}") return value @@ -1015,9 +1057,11 @@ def requires_component(comp): uint8_t = int_range(min=0, max=255) uint16_t = int_range(min=0, max=65535) uint32_t = int_range(min=0, max=4294967295) +uint64_t = int_range(min=0, max=18446744073709551615) hex_uint8_t = hex_int_range(min=0, max=255) hex_uint16_t = hex_int_range(min=0, max=65535) hex_uint32_t = hex_int_range(min=0, max=4294967295) +hex_uint64_t = hex_int_range(min=0, max=18446744073709551615) i2c_address = hex_uint8_t @@ -1073,6 +1117,7 @@ def invalid(message): def valid(value): + """A validator that is always valid and returns the value as-is.""" return value @@ -1151,10 +1196,8 @@ def one_of(*values, **kwargs): option = str(value) matches = difflib.get_close_matches(option, options_) if matches: - raise Invalid( - "Unknown value '{}', did you mean {}?" - "".format(value, ", ".join(f"'{x}'" for x in matches)) - ) + matches_str = ", ".join(f"'{x}'" for x in matches) + raise Invalid(f"Unknown value '{value}', did you mean {matches_str}?") raise Invalid(f"Unknown value '{value}', valid options are {options}.") return value @@ -1196,13 +1239,10 @@ def lambda_(value): entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value) if len(entity_id_parts) != 1: entity_ids = " ".join( - "'{}'".format(entity_id_parts[i]) for i in range(1, len(entity_id_parts), 2) + f"'{entity_id_parts[i]}'" for i in range(1, len(entity_id_parts), 2) ) raise Invalid( - "Lambda contains reference to entity-id-style ID {}. " - "The id() wrapper only works for ESPHome-internal types. For importing " - "states from Home Assistant use the 'homeassistant' sensor platforms." - "".format(entity_ids) + f"Lambda contains reference to entity-id-style ID {entity_ids}. The id() wrapper only works for ESPHome-internal types. For importing states from Home Assistant use the 'homeassistant' sensor platforms." ) return value @@ -1226,9 +1266,7 @@ def returning_lambda(value): def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise Invalid( - "Dimensions must have a length of two, not {}".format(len(value)) - ) + raise Invalid(f"Dimensions must have a length of two, not {len(value)}") try: width, height = int(value[0]), int(value[1]) except ValueError: @@ -1268,19 +1306,16 @@ def directory(value): if data["content"]: return value raise Invalid( - "Could not find directory '{}'. Please make sure it exists (full path: {})." - "".format(path, os.path.abspath(path)) + f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." ) if not os.path.exists(path): raise Invalid( - "Could not find directory '{}'. Please make sure it exists (full path: {})." - "".format(path, os.path.abspath(path)) + f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." ) if not os.path.isdir(path): raise Invalid( - "Path '{}' is not a directory (full path: {})." - "".format(path, os.path.abspath(path)) + f"Path '{path}' is not a directory (full path: {os.path.abspath(path)})." ) return value @@ -1307,19 +1342,16 @@ def file_(value): if data["content"]: return value raise Invalid( - "Could not find file '{}'. Please make sure it exists (full path: {})." - "".format(path, os.path.abspath(path)) + f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." ) if not os.path.exists(path): raise Invalid( - "Could not find file '{}'. Please make sure it exists (full path: {})." - "".format(path, os.path.abspath(path)) + f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." ) if not os.path.isfile(path): raise Invalid( - "Path '{}' is not a file (full path: {})." - "".format(path, os.path.abspath(path)) + f"Path '{path}' is not a file (full path: {os.path.abspath(path)})." ) return value @@ -1372,7 +1404,7 @@ def typed_schema(schemas, **kwargs): value = value.copy() schema_option = value.pop(key, default_schema_option) if schema_option is None: - raise Invalid(key + " not specified!") + raise Invalid(f"{key} not specified!") key_v = key_validator(schema_option) value = schemas[key_v](value) value[key] = key_v @@ -1391,18 +1423,32 @@ class GenerateID(Optional): class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" - def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): + def __init__( + self, + key, + esp8266=vol.UNDEFINED, + esp32=vol.UNDEFINED, + esp32_arduino=vol.UNDEFINED, + esp32_idf=vol.UNDEFINED, + ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) - self._esp32_default = vol.default_factory(esp32) + self._esp32_arduino_default = vol.default_factory( + esp32_arduino if esp32 is vol.UNDEFINED else esp32 + ) + self._esp32_idf_default = vol.default_factory( + esp32_idf if esp32 is vol.UNDEFINED else esp32 + ) @property def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32: - return self._esp32_default - raise ValueError + 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 + raise NotImplementedError @default.setter def default(self, value): @@ -1421,11 +1467,7 @@ class OnlyWith(Optional): @property def default(self): # pylint: disable=unsupported-membership-test - if self._component in CORE.raw_config or ( - CONF_PACKAGES in CORE.raw_config - and self._component - in {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()} - ): + if self._component in CORE.loaded_integrations: return self._default return vol.UNDEFINED @@ -1435,7 +1477,7 @@ class OnlyWith(Optional): pass -def _nameable_validator(config): +def _entity_base_validator(config): if CONF_NAME not in config and CONF_ID not in config: raise Invalid("At least one of 'id:' or 'name:' is required!") if CONF_NAME not in config: @@ -1469,8 +1511,7 @@ def validate_registry_entry(name, registry): value = {value: {}} if not isinstance(value, dict): raise Invalid( - "{} must consist of key-value mapping! Got {}" - "".format(name.title(), value) + f"{name.title()} must consist of key-value mapping! Got {value}" ) key = next((x for x in value if x not in ignore_keys), None) if key is None: @@ -1480,9 +1521,8 @@ def validate_registry_entry(name, registry): key2 = next((x for x in value if x != key and x not in ignore_keys), None) if key2 is not None: raise Invalid( - "Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " - "Did you forget to indent the block inside the {0}?" - "".format(name, key, key2) + f"Cannot have two {name}s in one item. Key '{key}' overrides '{key2}'! " + f"Did you forget to indent the block inside the {key}?" ) if value[key] is None: @@ -1533,17 +1573,14 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( MQTT_COMPONENT_SCHEMA = Schema( { - Optional(CONF_NAME): string, Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), Optional(CONF_AVAILABILITY): All( requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) ), - Optional(CONF_INTERNAL): boolean, } ) -MQTT_COMPONENT_SCHEMA.add_extra(_nameable_validator) MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( { @@ -1551,6 +1588,17 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( } ) +ENTITY_BASE_SCHEMA = Schema( + { + Optional(CONF_NAME): string, + Optional(CONF_INTERNAL): boolean, + Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, + Optional(CONF_ICON): icon, + } +) + +ENTITY_BASE_SCHEMA.add_extra(_entity_base_validator) + COMPONENT_SCHEMA = Schema({Optional(CONF_SETUP_PRIORITY): float_}) @@ -1588,3 +1636,87 @@ def url(value): if not parsed.scheme or not parsed.netloc: raise Invalid("Expected a URL scheme and host") return parsed.geturl() + + +def git_ref(value): + if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + raise Invalid("Not a valid git ref") + return value + + +def source_refresh(value: str): + if value.lower() == "always": + return source_refresh("0s") + if value.lower() == "never": + return source_refresh("1000y") + return positive_time_period_seconds(value) + + +@dataclass(frozen=True, order=True) +class Version: + major: int + minor: int + patch: int + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + @classmethod + def parse(cls, value: str) -> "Version": + match = re.match(r"(\d+).(\d+).(\d+)", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + return Version(major=major, minor=minor, patch=patch) + + +def version_number(value): + value = string_strict(value) + try: + return str(Version.parse(value)) + except ValueError as e: + raise Invalid("Not a version number") from e + + +def require_framework_version( + *, + esp_idf=None, + esp32_arduino=None, + esp8266_arduino=None, +): + def validator(value): + core_data = CORE.data[KEY_CORE] + framework = core_data[KEY_TARGET_FRAMEWORK] + if framework == "esp-idf": + if esp_idf is None: + raise Invalid("This feature is incompatible with esp-idf") + required = esp_idf + elif CORE.is_esp32 and framework == "arduino": + if esp32_arduino is None: + raise Invalid( + "This feature is incompatible with ESP32 using arduino framework" + ) + required = esp32_arduino + elif CORE.is_esp8266 and framework == "arduino": + if esp8266_arduino is None: + raise Invalid("This feature is incompatible with ESP8266") + required = esp8266_arduino + else: + raise NotImplementedError + if core_data[KEY_FRAMEWORK_VERSION] < required: + raise Invalid( + f"This feature requires at least framework version {required}" + ) + return value + + return validator + + +@contextmanager +def suppress_invalid(): + try: + yield + except vol.Invalid: + pass diff --git a/esphome/const.py b/esphome/const.py index 11759d82ef..54d677a62e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,33 +1,17 @@ """Constants used by esphome.""" -MAJOR_VERSION = 1 -MINOR_VERSION = 20 -PATCH_VERSION = "0-dev" -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" - -ESP_PLATFORM_ESP32 = "ESP32" -ESP_PLATFORM_ESP8266 = "ESP8266" -ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] +__version__ = "2021.10.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" -# Lookup table from ESP32 arduino framework version to latest platformio -# package with that version -# See also https://github.com/platformio/platform-espressif32/releases -ARDUINO_VERSION_ESP32 = { - "dev": "https://github.com/platformio/platform-espressif32.git", - "1.0.6": "platformio/espressif32@3.2.0", - "1.0.5": "platformio/espressif32@3.1.1", - "1.0.4": "platformio/espressif32@3.0.0", - "1.0.3": "platformio/espressif32@1.10.0", - "1.0.2": "platformio/espressif32@1.9.0", - "1.0.1": "platformio/espressif32@1.7.0", - "1.0.0": "platformio/espressif32@1.5.0", -} +TARGET_PLATFORMS = ["esp32", "esp8266"] +TARGET_FRAMEWORKS = ["arduino", "esp-idf"] + # See also https://github.com/platformio/platform-espressif8266/releases ARDUINO_VERSION_ESP8266 = { "dev": "https://github.com/platformio/platform-espressif8266.git", + "3.0.1": "platformio/espressif8266@3.1.0", + "3.0.0": "platformio/espressif8266@3.0.0", "2.7.4": "platformio/espressif8266@2.6.2", "2.7.3": "platformio/espressif8266@2.6.1", "2.7.2": "platformio/espressif8266@2.6.0", @@ -55,9 +39,11 @@ CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" CONF_ACTION_ID = "action_id" +CONF_ACTION_STATE_TOPIC = "action_state_topic" CONF_ACTIVE_POWER = "active_power" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" +CONF_ADVANCED = "advanced" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_AND = "and" @@ -71,14 +57,19 @@ CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" CONF_AUTH = "auth" CONF_AUTO_MODE = "auto_mode" +CONF_AUTOCONF = "autoconf" CONF_AUTOMATION_ID = "automation_id" CONF_AVAILABILITY = "availability" CONF_AWAY = "away" +CONF_AWAY_COMMAND_TOPIC = "away_command_topic" CONF_AWAY_CONFIG = "away_config" +CONF_AWAY_STATE_TOPIC = "away_state_topic" CONF_BACKLIGHT_PIN = "backlight_pin" +CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_VOLTAGE = "battery_voltage" CONF_BAUD_RATE = "baud_rate" +CONF_BEEPER = "beeper" CONF_BELOW = "below" CONF_BINARY = "binary" CONF_BINARY_SENSOR = "binary_sensor" @@ -90,6 +81,7 @@ CONF_BLE_SERVER_ID = "ble_server_id" CONF_BLUE = "blue" CONF_BOARD = "board" CONF_BOARD_FLASH_MODE = "board_flash_mode" +CONF_BORDER = "border" CONF_BRANCH = "branch" CONF_BRIGHTNESS = "brightness" CONF_BROKER = "broker" @@ -98,6 +90,7 @@ CONF_BUFFER_SIZE = "buffer_size" CONF_BUILD_PATH = "build_path" CONF_BUS_VOLTAGE = "bus_voltage" CONF_BUSY_PIN = "busy_pin" +CONF_CALCULATED_LUX = "calculated_lux" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" @@ -121,7 +114,10 @@ CONF_CODE = "code" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" +CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" +CONF_COLOR_INTERLOCK = "color_interlock" +CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" @@ -133,9 +129,12 @@ CONF_COMPONENTS = "components" CONF_CONDITION = "condition" CONF_CONDITION_ID = "condition_id" CONF_CONDUCTIVITY = "conductivity" +CONF_CONSTANT_BRIGHTNESS = "constant_brightness" CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" +CONF_COOL_DEADBAND = "cool_deadband" CONF_COOL_MODE = "cool_mode" +CONF_COOL_OVERRUN = "cool_overrun" CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" CONF_COURSE = "course" @@ -146,6 +145,7 @@ CONF_CSS_URL = "css_url" CONF_CURRENT = "current" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" +CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" CONF_CUSTOM_FAN_MODES = "custom_fan_modes" CONF_CUSTOM_PRESET = "custom_preset" @@ -159,8 +159,11 @@ CONF_DATA_TEMPLATE = "data_template" CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" +CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" +CONF_DECAY_MODE = "decay_mode" CONF_DECELERATION = "deceleration" +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" @@ -168,11 +171,13 @@ CONF_DELAY = "delay" CONF_DELTA = "delta" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" +CONF_DEVICE_FACTOR = "device_factor" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" +CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCOVERY = "discovery" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_DISCOVERY_RETAIN = "discovery_retain" @@ -188,15 +193,15 @@ CONF_DUMP = "dump" CONF_DURATION = "duration" CONF_EAP = "eap" CONF_ECHO_PIN = "echo_pin" +CONF_ECO2 = "eco2" CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" -CONF_ENABLE_MDNS = "enable_mdns" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" CONF_ENTITY_ID = "entity_id" -CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" +CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" CONF_EVENT = "event" @@ -209,6 +214,7 @@ CONF_FALLING_EDGE = "falling_edge" CONF_FAMILY = "family" CONF_FAN_MODE = "fan_mode" CONF_FAN_MODE_AUTO_ACTION = "fan_mode_auto_action" +CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_DIFFUSE_ACTION = "fan_mode_diffuse_action" CONF_FAN_MODE_FOCUS_ACTION = "fan_mode_focus_action" CONF_FAN_MODE_HIGH_ACTION = "fan_mode_high_action" @@ -217,29 +223,39 @@ CONF_FAN_MODE_MEDIUM_ACTION = "fan_mode_medium_action" CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" +CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" CONF_FAN_ONLY_ACTION = "fan_only_action" +CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer" +CONF_FAN_ONLY_COOLING = "fan_only_cooling" 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_FILE = "file" +CONF_FILES = "files" CONF_FILTER = "filter" CONF_FILTER_OUT = "filter_out" CONF_FILTERS = "filters" CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" +CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" +CONF_FRAMEWORK = "framework" CONF_FREQUENCY = "frequency" CONF_FROM = "from" +CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_GAIN = "gain" CONF_GAMMA_CORRECT = "gamma_correct" CONF_GAS_RESISTANCE = "gas_resistance" CONF_GATEWAY = "gateway" +CONF_GLASS_ATTENUATION_FACTOR = "glass_attenuation_factor" CONF_GLYPHS = "glyphs" CONF_GPIO = "gpio" CONF_GREEN = "green" @@ -247,7 +263,9 @@ CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" +CONF_HEAT_DEADBAND = "heat_deadband" CONF_HEAT_MODE = "heat_mode" +CONF_HEAT_OVERRUN = "heat_overrun" CONF_HEATER = "heater" CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" @@ -260,6 +278,9 @@ CONF_HUMIDITY = "humidity" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" +CONF_IBEACON_MAJOR = "ibeacon_major" +CONF_IBEACON_MINOR = "ibeacon_minor" +CONF_IBEACON_UUID = "ibeacon_uuid" CONF_ICON = "icon" CONF_ID = "id" CONF_IDENTITY = "identity" @@ -268,6 +289,7 @@ CONF_IDLE_ACTION = "idle_action" CONF_IDLE_LEVEL = "idle_level" CONF_IDLE_TIME = "idle_time" CONF_IF = "if" +CONF_IGNORE_EFUSE_MAC_CRC = "ignore_efuse_mac_crc" CONF_IIR_FILTER = "iir_filter" CONF_ILLUMINANCE = "illuminance" CONF_IMPEDANCE = "impedance" @@ -276,8 +298,11 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" +CONF_INFRARED = "infrared" CONF_INITIAL_MODE = "initial_mode" +CONF_INITIAL_OPTION = "initial_option" CONF_INITIAL_VALUE = "initial_value" +CONF_INPUT = "input" CONF_INTEGRATION_TIME = "integration_time" CONF_INTENSITY = "intensity" CONF_INTERLOCK = "interlock" @@ -299,13 +324,17 @@ CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" CONF_LATITUDE = "latitude" +CONF_LEGEND = "legend" CONF_LENGTH = "length" CONF_LEVEL = "level" CONF_LG = "lg" CONF_LIBRARIES = "libraries" CONF_LIGHT = "light" +CONF_LIGHT_ID = "light_id" CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_THRESHOLD = "lightning_threshold" +CONF_LINE_THICKNESS = "line_thickness" +CONF_LINE_TYPE = "line_type" CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" CONF_LOG_TOPIC = "log_topic" @@ -320,11 +349,14 @@ CONF_MAKE_ID = "make_id" CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" +CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" +CONF_MAX_HEATING_RUN_TIME = "max_heating_run_time" CONF_MAX_LENGTH = "max_length" CONF_MAX_LEVEL = "max_level" CONF_MAX_POWER = "max_power" +CONF_MAX_RANGE = "max_range" CONF_MAX_REFRESH_RATE = "max_refresh_rate" CONF_MAX_SPEED = "max_speed" CONF_MAX_TEMPERATURE = "max_temperature" @@ -335,15 +367,26 @@ CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" +CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" +CONF_MIN_COOLING_RUN_TIME = "min_cooling_run_time" +CONF_MIN_FAN_MODE_SWITCHING_TIME = "min_fan_mode_switching_time" +CONF_MIN_FANNING_OFF_TIME = "min_fanning_off_time" +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_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" +CONF_MIN_RANGE = "min_range" CONF_MIN_TEMPERATURE = "min_temperature" CONF_MIN_VALUE = "min_value" CONF_MINUTE = "minute" CONF_MINUTES = "minutes" 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_MONTHS = "months" @@ -355,6 +398,7 @@ CONF_MQTT_ID = "mqtt_id" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" CONF_NAME = "name" +CONF_NAME_FONT = "name_font" CONF_NBITS = "nbits" CONF_NEC = "nec" CONF_NETWORKS = "networks" @@ -390,6 +434,7 @@ CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" CONF_ON_SHUTDOWN = "on_shutdown" +CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" @@ -401,10 +446,13 @@ CONF_ON_VALUE = "on_value" CONF_ON_VALUE_RANGE = "on_value_range" CONF_ONE = "one" CONF_OPEN_ACTION = "open_action" +CONF_OPEN_DRAIN = "open_drain" CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" CONF_OPTIMISTIC = "optimistic" +CONF_OPTION = "option" +CONF_OPTIONS = "options" CONF_OR = "or" CONF_OSCILLATING = "oscillating" CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" @@ -436,10 +484,19 @@ CONF_PINS = "pins" CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" CONF_PLATFORMIO_OPTIONS = "platformio_options" +CONF_PM_0_3UM = "pm_0_3um" +CONF_PM_0_5UM = "pm_0_5um" CONF_PM_1_0 = "pm_1_0" +CONF_PM_1_0_STD = "pm_1_0_std" +CONF_PM_1_0UM = "pm_1_0um" CONF_PM_10_0 = "pm_10_0" +CONF_PM_10_0_STD = "pm_10_0_std" +CONF_PM_10_0UM = "pm_10_0um" CONF_PM_2_5 = "pm_2_5" +CONF_PM_2_5_STD = "pm_2_5_std" +CONF_PM_2_5UM = "pm_2_5um" CONF_PM_4_0 = "pm_4_0" +CONF_PM_5_0UM = "pm_5_0um" CONF_PM_SIZE = "pm_size" CONF_PMC_0_5 = "pmc_0_5" CONF_PMC_1_0 = "pmc_1_0" @@ -449,6 +506,8 @@ CONF_PMC_4_0 = "pmc_4_0" CONF_PORT = "port" CONF_POSITION = "position" CONF_POSITION_ACTION = "position_action" +CONF_POSITION_COMMAND_TOPIC = "position_command_topic" +CONF_POSITION_STATE_TOPIC = "position_state_topic" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" CONF_POWER_ON_VALUE = "power_on_value" @@ -463,22 +522,29 @@ CONF_PRIORITY = "priority" CONF_PROJECT = "project" CONF_PROTOCOL = "protocol" CONF_PULL_MODE = "pull_mode" +CONF_PULLDOWN = "pulldown" +CONF_PULLUP = "pullup" CONF_PULSE_LENGTH = "pulse_length" CONF_QOS = "qos" +CONF_RADON = "radon" +CONF_RADON_LONG_TERM = "radon_long_term" CONF_RANDOM = "random" CONF_RANGE = "range" CONF_RANGE_FROM = "range_from" CONF_RANGE_TO = "range_to" CONF_RATE = "rate" CONF_RAW = "raw" +CONF_RAW_DATA_ID = "raw_data_id" CONF_RC_CODE_1 = "rc_code_1" CONF_RC_CODE_2 = "rc_code_2" CONF_REACTIVE_POWER = "reactive_power" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" +CONF_REF = "ref" CONF_REFERENCE_RESISTANCE = "reference_resistance" CONF_REFERENCE_TEMPERATURE = "reference_temperature" +CONF_REFRESH = "refresh" CONF_REPEAT = "repeat" CONF_REPOSITORY = "repository" CONF_RESET_PIN = "reset_pin" @@ -498,7 +564,6 @@ CONF_ROTATION = "rotation" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" -CONF_RUN_CYCLES = "run_cycles" CONF_RUN_DURATION = "run_duration" CONF_RW_PIN = "rw_pin" CONF_RX_BUFFER_SIZE = "rx_buffer_size" @@ -508,6 +573,7 @@ CONF_SAFE_MODE = "safe_mode" CONF_SAMSUNG = "samsung" CONF_SATELLITES = "satellites" CONF_SCAN = "scan" +CONF_SCAN_RESULTS = "scan_results" CONF_SCL = "scl" CONF_SCL_PIN = "scl_pin" CONF_SDA = "sda" @@ -528,11 +594,16 @@ CONF_SERVERS = "servers" CONF_SERVICE = "service" CONF_SERVICE_UUID = "service_uuid" CONF_SERVICES = "services" +CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential" CONF_SETUP_MODE = "setup_mode" CONF_SETUP_PRIORITY = "setup_priority" +CONF_SHOW_LINES = "show_lines" +CONF_SHOW_UNITS = "show_units" +CONF_SHOW_VALUES = "show_values" CONF_SHUNT_RESISTANCE = "shunt_resistance" CONF_SHUNT_VOLTAGE = "shunt_voltage" CONF_SHUTDOWN_MESSAGE = "shutdown_message" +CONF_SINGLE_LIGHT_ID = "single_light_id" CONF_SIZE = "size" CONF_SLEEP_DURATION = "sleep_duration" CONF_SLEEP_PIN = "sleep_pin" @@ -542,27 +613,41 @@ CONF_SOURCE = "source" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" 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_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" CONF_SSL_FINGERPRINTS = "ssl_fingerprints" +CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" CONF_STATUS = "status" +CONF_STEP = "step" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" CONF_SUBNET = "subnet" CONF_SUBSTITUTIONS = "substitutions" +CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" +CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta" +CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action" +CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta" +CONF_SUPPORTED_FAN_MODES = "supported_fan_modes" +CONF_SUPPORTED_MODES = "supported_modes" +CONF_SUPPORTED_PRESETS = "supported_presets" +CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" CONF_SWING_HORIZONTAL_ACTION = "swing_horizontal_action" CONF_SWING_MODE = "swing_mode" +CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" +CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" CONF_SWING_OFF_ACTION = "swing_off_action" CONF_SWING_VERTICAL_ACTION = "swing_vertical_action" CONF_SWITCH_DATAPOINT = "switch_datapoint" @@ -572,8 +657,15 @@ CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" CONF_TARGET_TEMPERATURE = "target_temperature" +CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" +CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" CONF_TARGET_TEMPERATURE_HIGH = "target_temperature_high" +CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC = "target_temperature_high_command_topic" +CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC = "target_temperature_high_state_topic" CONF_TARGET_TEMPERATURE_LOW = "target_temperature_low" +CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topic" +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_STEP = "temperature_step" CONF_TEXT_SENSORS = "text_sensors" @@ -582,7 +674,9 @@ CONF_THRESHOLD = "threshold" CONF_THROTTLE = "throttle" CONF_TILT = "tilt" CONF_TILT_ACTION = "tilt_action" +CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" CONF_TILT_LAMBDA = "tilt_lambda" +CONF_TILT_STATE_TOPIC = "tilt_state_topic" CONF_TIME = "time" CONF_TIME_ID = "time_id" CONF_TIMEOUT = "timeout" @@ -594,6 +688,7 @@ CONF_TOLERANCE = "tolerance" CONF_TOPIC = "topic" CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOTAL = "total" +CONF_TRACES = "traces" CONF_TRANSITION_LENGTH = "transition_length" CONF_TRIGGER_ID = "trigger_id" CONF_TRIGGER_PIN = "trigger_pin" @@ -616,9 +711,11 @@ CONF_USE_ADDRESS = "use_address" CONF_USERNAME = "username" CONF_UUID = "uuid" CONF_VALUE = "value" +CONF_VALUE_FONT = "value_font" CONF_VARIABLES = "variables" CONF_VARIANT = "variant" CONF_VERSION = "version" +CONF_VISIBLE = "visible" CONF_VISUAL = "visual" CONF_VOLTAGE = "voltage" CONF_VOLTAGE_ATTENUATION = "voltage_attenuation" @@ -638,6 +735,8 @@ CONF_WILL_MESSAGE = "will_message" CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees" CONF_WIND_SPEED = "wind_speed" CONF_WINDOW_SIZE = "window_size" +CONF_X_GRID = "x_grid" +CONF_Y_GRID = "y_grid" CONF_ZERO = "zero" ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE" @@ -652,8 +751,10 @@ ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" ICON_BLUETOOTH = "mdi:bluetooth" +ICON_BLUR = "mdi:blur" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" +ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BUG = "mdi:bug" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" @@ -680,7 +781,9 @@ ICON_PERCENT = "mdi:percent" ICON_POWER = "mdi:power" ICON_PULSE = "mdi:pulse" ICON_RADIATOR = "mdi:radiator" +ICON_RADIOACTIVE = "mdi:radioactive" ICON_RESTART = "mdi:restart" +ICON_RESTART_ALERT = "mdi:restart-alert" ICON_ROTATE_RIGHT = "mdi:rotate-right" ICON_RULER = "mdi:ruler" ICON_SCALE = "mdi:scale" @@ -701,7 +804,9 @@ ICON_WEATHER_WINDY = "mdi:weather-windy" ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" +UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_CELSIUS = "°C" +UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" UNIT_DECIBEL = "dB" @@ -716,6 +821,10 @@ 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_KILOWATT = "kW" +UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" @@ -746,7 +855,6 @@ DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_CONNECTIVITY = "connectivity" DEVICE_CLASS_DOOR = "door" DEVICE_CLASS_GARAGE_DOOR = "garage_door" -DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_LIGHT = "light" DEVICE_CLASS_LOCK = "lock" @@ -761,24 +869,37 @@ DEVICE_CLASS_PROBLEM = "problem" DEVICE_CLASS_SAFETY = "safety" DEVICE_CLASS_SMOKE = "smoke" DEVICE_CLASS_SOUND = "sound" +DEVICE_CLASS_UPDATE = "update" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_POWER = "power" # device classes of sensor component -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" +DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" -DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" -DEVICE_CLASS_TEMPERATURE = "temperature" +DEVICE_CLASS_MONETARY = "monetary" +DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" +DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" +DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PM1 = "pm1" +DEVICE_CLASS_PM10 = "pm10" +DEVICE_CLASS_PM25 = "pm25" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" +DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" # state classes @@ -786,3 +907,11 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" + +# The state represents a total that only increases, a decrease is considered a reset. +STATE_CLASS_TOTAL_INCREASING = "total_increasing" + +KEY_CORE = "core" +KEY_TARGET_PLATFORM = "target_platform" +KEY_TARGET_FRAMEWORK = "target_framework" +KEY_FRAMEWORK_VERSION = "framework_version" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 1841dfd8be..8bdef3a4ea 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -2,16 +2,17 @@ import logging import math import os import re -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union from esphome.const import ( - CONF_ARDUINO_VERSION, CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_ETHERNET, CONF_WIFI, - SOURCE_FILE_EXTENSIONS, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, ) from esphome.coroutine import FakeAwaitable as _FakeAwaitable from esphome.coroutine import FakeEventLoop as _FakeEventLoop @@ -23,6 +24,7 @@ from esphome.util import OrderedDict if TYPE_CHECKING: from ..cpp_generator import MockObj, MockObjClass, Statement + from ..types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -310,7 +312,7 @@ class ID: if self.id is None: base = str(self.type).replace("::", "_").lower() name = "".join(c for c in base if c.isalnum() or c == "_") - used = set(registered_ids) | set(RESERVED_IDS) + used = set(registered_ids) | set(RESERVED_IDS) | CORE.loaded_integrations self.id = ensure_unique_string(name, used) return self.id @@ -407,19 +409,28 @@ class Define: class Library: - def __init__(self, name, version): + def __init__(self, name, version, repository=None): self.name = name self.version = version + self.repository = repository + + def __str__(self): + return self.as_lib_dep @property def as_lib_dep(self): + if self.repository is not None: + if self.name is not None: + return f"{self.name}={self.repository}" + return self.repository + if self.version is None: return self.name return f"{self.name}@{self.version}" @property def as_tuple(self): - return self.name, self.version + return self.name, self.version, self.repository def __hash__(self): return hash(self.as_tuple) @@ -430,19 +441,6 @@ class Library: return NotImplemented -def find_source_files(file): - files = set() - directory = os.path.abspath(os.path.dirname(file)) - for f in os.listdir(directory): - if not os.path.isfile(os.path.join(directory, f)): - continue - _, ext = os.path.splitext(f) - if ext.lower() not in SOURCE_FILE_EXTENSIONS: - continue - files.add(f) - return files - - # pylint: disable=too-many-instance-attributes,too-many-public-methods class EsphomeCore: def __init__(self): @@ -453,18 +451,15 @@ class EsphomeCore: self.ace = False # The name of the node self.name: 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 = {} # The relative path to the configuration YAML self.config_path: Optional[str] = None # The relative path to where all build files are stored self.build_path: Optional[str] = None - # The platform (ESP8266, ESP32) of this device - self.esp_platform: Optional[str] = None - # The board that's used (for example nodemcuv2) - self.board: Optional[str] = None - # The full raw configuration - self.raw_config: Optional[ConfigType] = 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) @@ -483,6 +478,8 @@ class EsphomeCore: 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() + # 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 self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared @@ -493,11 +490,9 @@ class EsphomeCore: def reset(self): self.dashboard = False self.name = None + self.data = {} self.config_path = None self.build_path = None - self.esp_platform = None - self.board = None - self.raw_config = None self.config = None self.event_loop = _FakeEventLoop() self.task_counter = 0 @@ -507,6 +502,7 @@ class EsphomeCore: self.libraries = [] self.build_flags = set() self.defines = set() + self.platformio_options = {} self.loaded_integrations = set() self.component_ids = set() @@ -533,13 +529,6 @@ class EsphomeCore: return None - @property - def arduino_version(self) -> str: - if self.config is None: - raise ValueError("Config has not been loaded yet") - - return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] - @property def config_dir(self): return os.path.dirname(self.config_path) @@ -553,6 +542,9 @@ class EsphomeCore: path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.config_dir, path_) + def relative_internal_path(self, *path: str) -> str: + return self.relative_config_path(".esphome", *path) + def relative_build_path(self, *path): # pylint: disable=no-value-for-parameter path_ = os.path.expanduser(os.path.join(*path)) @@ -575,17 +567,29 @@ class EsphomeCore: def firmware_bin(self): return self.relative_pioenvs_path(self.name, "firmware.bin") + @property + def target_platform(self): + return self.data[KEY_CORE][KEY_TARGET_PLATFORM] + @property def is_esp8266(self): - if self.esp_platform is None: - raise ValueError("No platform specified") - return self.esp_platform == "ESP8266" + return self.target_platform == "esp8266" @property def is_esp32(self): - if self.esp_platform is None: - raise ValueError("No platform specified") - return self.esp_platform == "ESP32" + return self.target_platform == "esp32" + + @property + def target_framework(self): + return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK] + + @property + def using_arduino(self): + return self.target_framework == "arduino" + + @property + def using_esp_idf(self): + return self.target_framework == "esp-idf" def add_job(self, func, *args, **kwargs): self.event_loop.add_job(func, *args, **kwargs) @@ -603,8 +607,7 @@ class EsphomeCore: expression = statement(expression) if not isinstance(expression, Statement): raise ValueError( - "Add '{}' must be expression or statement, not {}" - "".format(expression, type(expression)) + f"Add '{expression}' must be expression or statement, not {type(expression)}" ) self.main_statements.append(expression) @@ -618,8 +621,7 @@ class EsphomeCore: expression = statement(expression) if not isinstance(expression, Statement): raise ValueError( - "Add '{}' must be expression or statement, not {}" - "".format(expression, type(expression)) + f"Add '{expression}' must be expression or statement, not {type(expression)}" ) self.global_statements.append(expression) _LOGGER.debug("Adding global: %s", expression) @@ -628,13 +630,25 @@ class EsphomeCore: def add_library(self, library): if not isinstance(library, Library): raise ValueError( - "Library {} must be instance of Library, not {}" - "".format(library, type(library)) + f"Library {library} must be instance of Library, not {type(library)}" ) - _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: - if other.name != library.name: + if other.name != library.name or other.name is None or library.name is None: continue + if other.repository is not None: + if library.repository is None or other.repository == library.repository: + # Other is using a/the same repository, takes precendence + break + raise ValueError( + f"Adding named Library with repository failed! Libraries {library} and {other} " + "requested with conflicting repositories!" + ) + + if library.repository is not None: + # This is more specific since its using a repository + self.libraries.remove(other) + continue + if library.version is None: # Other requirement is more specific break @@ -646,11 +660,11 @@ class EsphomeCore: break raise ValueError( - "Version pinning failed! Libraries {} and {} " + f"Version pinning failed! Libraries {library} and {other} " "requested with conflicting versions!" - "".format(library, other) ) else: + _LOGGER.debug("Adding library: %s", library) self.libraries.append(library) return library @@ -666,13 +680,20 @@ class EsphomeCore: pass else: raise ValueError( - "Define {} must be string or Define, not {}" - "".format(define, type(define)) + f"Define {define} must be string or Define, not {type(define)}" ) self.defines.add(define) _LOGGER.debug("Adding define: %s", define) return define + def add_platformio_option(self, key: str, value: Union[str, List[str]]) -> None: + new_val = value + old_val = self.platformio_options.get(key) + if isinstance(old_val, list): + assert isinstance(value, list) + new_val = old_val + value + self.platformio_options[key] = new_val + def _get_variable_generator(self, id): while True: try: @@ -752,6 +773,3 @@ class EnumValue: CORE = EsphomeCore() - -ConfigType = Dict[str, Any] -CoreType = EsphomeCore diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 17a2725de5..a4d61f819c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -1,7 +1,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/version.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -19,7 +19,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component already registered! (%p)", c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); return; } } @@ -53,35 +53,29 @@ void Application::setup() { } this->app_state_ = new_app_state; yield(); + this->feed_wdt(); } while (!component->can_proceed()); } ESP_LOGI(TAG, "setup() finished successfully!"); this->schedule_dump_config(); this->calculate_looping_components_(); - - // Dummy function to link some symbols into the binary. - force_link_symbols(); } void Application::loop() { uint32_t new_app_state = 0; - const uint32_t start = millis(); this->scheduler.call(); for (Component *component : this->looping_components_) { - component->call(); + { + WarnIfComponentBlockingGuard guard{component}; + component->call(); + } new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; this->feed_wdt(); } this->app_state_ = new_app_state; - const uint32_t end = millis(); - if (end - start > 200) { - ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f); - ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); - } - const uint32_t now = millis(); if (HighFrequencyLoopRequester::is_high_frequency()) { @@ -108,22 +102,17 @@ void Application::loop() { #endif } - this->components_[this->dump_config_at_]->dump_config(); + this->components_[this->dump_config_at_]->call_dump_config(); this->dump_config_at_++; } } -void ICACHE_RAM_ATTR HOT Application::feed_wdt() { - static uint32_t LAST_FEED = 0; +void IRAM_ATTR HOT Application::feed_wdt() { + static uint32_t last_feed = 0; uint32_t now = millis(); - if (now - LAST_FEED > 3) { -#ifdef ARDUINO_ARCH_ESP8266 - ESP.wdtFeed(); -#endif -#ifdef ARDUINO_ARCH_ESP32 - yield(); -#endif - LAST_FEED = now; + if (now - last_feed > 3) { + arch_feed_wdt(); + last_feed = now; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { status_led::global_status_led->call(); @@ -135,11 +124,7 @@ void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot..."); for (auto *comp : this->components_) comp->on_shutdown(); - ESP.restart(); - // restart() doesn't always end execution - while (true) { - yield(); - } + arch_restart(); } void Application::safe_reboot() { ESP_LOGI(TAG, "Rebooting safely..."); @@ -147,11 +132,7 @@ void Application::safe_reboot() { comp->on_safe_shutdown(); for (auto *comp : this->components_) comp->on_shutdown(); - ESP.restart(); - // restart() doesn't always end execution - while (true) { - yield(); - } + arch_restart(); } void Application::calculate_looping_components_() { diff --git a/esphome/core/application.h b/esphome/core/application.h index c674210ac0..5c1483d301 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -32,19 +32,25 @@ #ifdef USE_COVER #include "esphome/components/cover/cover.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { class Application { public: void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { + this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { this->name_ = name + "-" + get_mac_address().substr(6); } else { this->name_ = name; } this->compilation_time_ = compilation_time; - global_preferences.begin(); } #ifdef USE_BINARY_SENSOR @@ -81,6 +87,14 @@ class Application { void register_light(light::LightState *light) { this->lights_.push_back(light); } #endif +#ifdef USE_NUMBER + void register_number(number::Number *number) { this->numbers_.push_back(number); } +#endif + +#ifdef USE_SELECT + void register_select(select::Select *select) { this->selects_.push_back(select); } +#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"); @@ -97,6 +111,8 @@ class Application { /// Get the name of this Application set by set_name(). const std::string &get_name() const { return this->name_; } + 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_; } /** Set the target interval with which to run the loop() calls. @@ -205,6 +221,24 @@ class Application { 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_) + 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_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif Scheduler scheduler; @@ -215,6 +249,8 @@ class Application { void calculate_looping_components_(); + void feed_wdt_arch_(); + std::vector components_{}; std::vector looping_components_{}; @@ -242,9 +278,16 @@ class Application { #ifdef USE_LIGHT std::vector lights_{}; #endif +#ifdef USE_NUMBER + std::vector numbers_{}; +#endif +#ifdef USE_SELECT + std::vector selects_{}; +#endif std::string name_; std::string compilation_time_; + bool name_add_mac_suffix_; uint32_t last_loop_{0}; uint32_t loop_interval_{16}; int dump_config_at_{-1}; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index fa49786d1d..d97d369d33 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -228,6 +228,8 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} + TEMPLATABLE_VALUE(uint32_t, timeout_value) + void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. @@ -238,6 +240,12 @@ template class WaitUntilAction : public Action, public Co return; } this->var_ = std::make_tuple(x...); + + if (this->timeout_value_.has_value()) { + auto f = std::bind(&WaitUntilAction::play_next_, this, x...); + this->set_timeout("timeout", this->timeout_value_.value(x...), f); + } + this->loop(); } @@ -249,6 +257,8 @@ template class WaitUntilAction : public Action, public Co return; } + this->cancel_timeout("timeout"); + this->play_next_tuple_(this->var_); } @@ -257,6 +267,8 @@ template class WaitUntilAction : public Action, public Co void play(Ts... x) override { /* ignore - see play_complex */ } + void stop() override { this->cancel_timeout("timeout"); } + protected: Condition *condition_; std::tuple var_{}; diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp new file mode 100644 index 0000000000..58d995db2f --- /dev/null +++ b/esphome/core/color.cpp @@ -0,0 +1,11 @@ +#include "esphome/core/color.h" + +namespace esphome { + +const Color Color::BLACK(0, 0, 0, 0); +const Color Color::WHITE(255, 255, 255, 255); + +const Color COLOR_BLACK(0, 0, 0, 0); +const Color COLOR_WHITE(255, 255, 255, 255); + +} // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 6e8c769d10..c9ca3bcfc3 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -38,10 +38,10 @@ struct Color { g(green), b(blue), w(white) {} - inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), - g((colorcode >> 8) & 0xFF), - b((colorcode >> 0) & 0xFF), - w((colorcode >> 24) & 0xFF) {} + 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 bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } inline Color &operator=(const Color &rhs) ALWAYS_INLINE { // NOLINT @@ -143,8 +143,14 @@ struct Color { Color fade_to_black(uint8_t amnt) { return *this * amnt; } Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } + + static const Color BLACK; + static const Color WHITE; }; -static const Color COLOR_BLACK(0, 0, 0); -static const Color COLOR_WHITE(255, 255, 255, 255); -}; // namespace esphome +ESPDEPRECATED("Use Color::BLACK instead of COLOR_BLACK", "v1.21") +extern const Color COLOR_BLACK; +ESPDEPRECATED("Use Color::WHITE instead of COLOR_WHITE", "v1.21") +extern const Color COLOR_WHITE; + +} // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 09c91fbb0c..5692194a91 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -1,7 +1,7 @@ #include "esphome/core/component.h" #include "esphome/core/application.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; const float LATE = -100.0f; @@ -63,8 +64,9 @@ 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(); } + uint32_t Component::get_component_state() const { return this->component_state_; } void Component::call() { uint32_t state = this->component_state_ & COMPONENT_STATE_MASK; @@ -92,8 +94,13 @@ void Component::call() { break; } } +const char *Component::get_component_source() const { + if (this->component_source_ == nullptr) + return ""; + return this->component_source_; +} void Component::mark_failed() { - ESP_LOGE(TAG, "Component was marked as failed."); + ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); @@ -137,7 +144,7 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) } void Component::dump_config() {} float Component::get_actual_setup_priority() const { - if (isnan(this->setup_priority_override_)) + if (std::isnan(this->setup_priority_override_)) return this->get_setup_priority(); return this->setup_priority_override_; } @@ -170,21 +177,16 @@ void PollingComponent::call_setup() { 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; } -const std::string &Nameable::get_name() const { return this->name_; } -void Nameable::set_name(const std::string &name) { - this->name_ = name; - this->calc_object_id_(); +WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) + : started_(millis()), component_(component) {} +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."); + ; + } } -Nameable::Nameable(std::string name) : name_(std::move(name)) { this->calc_object_id_(); } - -const std::string &Nameable::get_object_id() { return this->object_id_; } -bool Nameable::is_internal() const { return this->internal_; } -void Nameable::set_internal(bool internal) { this->internal_ = internal; } -void Nameable::calc_object_id_() { - this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST); - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(this->object_id_); -} -uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 001620fe4a..a1afc17c2c 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -2,7 +2,7 @@ #include #include -#include "Arduino.h" +#include #include "esphome/core/optional.h" @@ -22,13 +22,15 @@ extern const float IO; extern const float HARDWARE; /// For components that import data from directly connected sensors like DHT. extern const float DATA; -/// Alias for DATA (here for compatability reasons) +/// Alias for DATA (here for compatibility reasons) extern const float HARDWARE_LATE; /// For components that use data from sensors like displays extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +/// For components that should be initialized after WiFi and before API is connected. +extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. extern const float AFTER_WIFI; /// For components that should be initialized after a data connection (API/MQTT) is connected. @@ -38,8 +40,12 @@ extern const float LATE; } // namespace setup_priority +static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; + #define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() < 100) { \ + if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ + ESP_LOGCONFIG(TAG, " Update Interval: never"); \ + } else if (this->get_update_interval() < 100) { \ ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ } else { \ ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ @@ -130,9 +136,24 @@ class Component { bool has_overridden_loop() const; + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core, and should not be called manually. + */ + void set_component_source(const char *source) { component_source_ = source; } + /** Get the integration where this component was declared as a string. + * + * Returns "" if source not set + */ + const char *get_component_source() const; + protected: + friend class Application; + virtual void call_loop(); virtual void call_setup(); + virtual void call_dump_config(); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). @@ -201,6 +222,7 @@ class Component { uint32_t component_state_{0x0000}; ///< State of this component. float setup_priority_override_{NAN}; + const char *component_source_ = nullptr; }; /** This class simplifies creating components that periodically check a state. @@ -242,29 +264,14 @@ class PollingComponent : public Component { uint32_t update_interval_; }; -/// Helper class that enables naming of objects so that it doesn't have to be re-implement every time. -class Nameable { +class WarnIfComponentBlockingGuard { public: - Nameable() : Nameable("") {} - explicit Nameable(std::string name); - const std::string &get_name() const; - void set_name(const std::string &name); - /// Get the sanitized name of this nameable as an ID. Caching it internally. - const std::string &get_object_id(); - uint32_t get_object_id_hash(); - - bool is_internal() const; - void set_internal(bool internal); + WarnIfComponentBlockingGuard(Component *component); + ~WarnIfComponentBlockingGuard(); protected: - virtual uint32_t hash_base() = 0; - - void calc_object_id_(); - - std::string name_; - std::string object_id_; - uint32_t object_id_hash_; - bool internal_{false}; + uint32_t started_; + Component *component_; }; } // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index fd4b7088cc..bbdfcf124c 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -4,7 +4,7 @@ import re import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation, pins +from esphome import automation from esphome.const import ( CONF_ARDUINO_VERSION, CONF_BOARD, @@ -12,6 +12,7 @@ from esphome.const import ( CONF_BUILD_PATH, CONF_COMMENT, CONF_ESPHOME, + CONF_FRAMEWORK, CONF_INCLUDES, CONF_LIBRARIES, CONF_NAME, @@ -23,11 +24,10 @@ from esphome.const import ( CONF_PRIORITY, CONF_PROJECT, CONF_TRIGGER_ID, - CONF_ESP8266_RESTORE_FROM_FLASH, - ARDUINO_VERSION_ESP8266, - ARDUINO_VERSION_ESP32, + CONF_TYPE, CONF_VERSION, - ESP_PLATFORMS, + KEY_CORE, + TARGET_PLATFORMS, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files @@ -50,84 +50,6 @@ VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix" -def validate_board(value): - if CORE.is_esp8266: - board_pins = pins.ESP8266_BOARD_PINS - elif CORE.is_esp32: - board_pins = pins.ESP32_BOARD_PINS - else: - raise NotImplementedError - - if value not in board_pins: - raise cv.Invalid( - "Could not find board '{}'. Valid boards are {}".format( - value, ", ".join(sorted(board_pins.keys())) - ) - ) - return value - - -validate_platform = cv.one_of(*ESP_PLATFORMS, upper=True) - -PLATFORMIO_ESP8266_LUT = { - **ARDUINO_VERSION_ESP8266, - # Keep this in mind when updating the recommended version: - # * New framework historically have had some regressions, especially for WiFi, BLE and the - # bootloader system. The new version needs to be thoroughly validated before changing the - # recommended version as otherwise a bunch of devices could be bricked - # * The docker images need to be updated to ship the new recommended version, in order not - # to DDoS platformio servers. - # Update this file: https://github.com/esphome/esphome-docker-base/blob/master/platformio.ini - "RECOMMENDED": ARDUINO_VERSION_ESP8266["2.7.4"], - "LATEST": "espressif8266", - "DEV": ARDUINO_VERSION_ESP8266["dev"], -} - -PLATFORMIO_ESP32_LUT = { - **ARDUINO_VERSION_ESP32, - # See PLATFORMIO_ESP8266_LUT for considerations when changing the recommended version - "RECOMMENDED": ARDUINO_VERSION_ESP32["1.0.6"], - "LATEST": "espressif32", - "DEV": ARDUINO_VERSION_ESP32["dev"], -} - - -def validate_arduino_version(value): - value = cv.string_strict(value) - value_ = value.upper() - if CORE.is_esp8266: - if ( - VERSION_REGEX.match(value) is not None - and value_ not in PLATFORMIO_ESP8266_LUT - ): - raise cv.Invalid( - "Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif8266@".format(value) - ) - if value_ in PLATFORMIO_ESP8266_LUT: - return PLATFORMIO_ESP8266_LUT[value_] - return value - if CORE.is_esp32: - if ( - VERSION_REGEX.match(value) is not None - and value_ not in PLATFORMIO_ESP32_LUT - ): - raise cv.Invalid( - "Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif32@".format(value) - ) - if value_ in PLATFORMIO_ESP32_LUT: - return PLATFORMIO_ESP32_LUT[value_] - return value - raise NotImplementedError - - -def default_build_path(): - return CORE.name - - VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} @@ -140,8 +62,7 @@ def valid_include(value): _, ext = os.path.splitext(value) if ext not in VALID_INCLUDE_EXTS: raise cv.Invalid( - "Include has invalid file extension {} - valid extensions are {}" - "".format(ext, ", ".join(VALID_INCLUDE_EXTS)) + f"Include has invalid file extension {ext} - valid extensions are {', '.join(VALID_INCLUDE_EXTS)}" ) return value @@ -155,27 +76,17 @@ def valid_project_name(value: str): return value +CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONFIG_SCHEMA = cv.Schema( { - cv.Required(CONF_NAME): cv.valid_name, - cv.Required(CONF_PLATFORM): cv.one_of("ESP8266", "ESP32", upper=True), - cv.Required(CONF_BOARD): validate_board, + cv.Required(CONF_NAME): cv.hostname, cv.Optional(CONF_COMMENT): cv.string, - cv.Optional( - CONF_ARDUINO_VERSION, default="recommended" - ): validate_arduino_version, - cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, + cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { cv.string_strict: cv.Any([cv.string], cv.string), } ), - cv.SplitDefault(CONF_ESP8266_RESTORE_FROM_FLASH, esp8266=False): cv.All( - cv.only_on_esp8266, cv.boolean - ), - cv.SplitDefault(CONF_BOARD_FLASH_MODE, esp8266="dout"): cv.one_of( - *BUILD_FLASH_MODES, lower=True - ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -201,49 +112,84 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_VERSION): cv.string_strict, } ), - cv.Optional("esphome_core_version"): cv.invalid( - "The esphome_core_version option has been " - "removed in 1.13 - the esphome core source " - "files are now bundled with ESPHome." - ), } ) PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - cv.Required(CONF_PLATFORM): validate_platform, + cv.Optional(CONF_BUILD_PATH): cv.string, + # Compat options, these were moved to target-platform specific sections + # but we'll keep these around for a long time because every config would + # be impacted + cv.Optional(CONF_PLATFORM): cv.one_of(*TARGET_PLATFORMS, lower=True), + cv.Optional(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): cv.valid, + cv.Optional(CONF_BOARD_FLASH_MODE): cv.valid, + cv.Optional(CONF_ARDUINO_VERSION): cv.valid, }, extra=cv.ALLOW_EXTRA, ) -PRELOAD_CONFIG_SCHEMA2 = PRELOAD_CONFIG_SCHEMA.extend( - { - cv.Required(CONF_BOARD): validate_board, - cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, - } -) +def preload_core_config(config, result): + with cv.prepend_path(CONF_ESPHOME): + conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) -def preload_core_config(config): - core_key = "esphome" - if "esphomeyaml" in config: - _LOGGER.warning( - "The esphomeyaml section has been renamed to esphome in 1.11.0. " - "Please replace 'esphomeyaml:' in your configuration with 'esphome:'." + CORE.name = conf[CONF_NAME] + CORE.data[KEY_CORE] = {} + + if CONF_BUILD_PATH not in conf: + conf[CONF_BUILD_PATH] = CORE.name + CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH]) + + has_oldstyle = CONF_PLATFORM in conf + newstyle_found = [key for key in TARGET_PLATFORMS if key in config] + oldstyle_opts = [ + CONF_ESP8266_RESTORE_FROM_FLASH, + CONF_BOARD_FLASH_MODE, + CONF_ARDUINO_VERSION, + CONF_BOARD, + ] + + if not has_oldstyle and not newstyle_found: + raise cv.Invalid("Platform missing for core options!", [CONF_ESPHOME]) + if has_oldstyle and newstyle_found: + raise cv.Invalid( + f"Please remove the `platform` key from the [esphome] block. You're already using the new style with the [{conf[CONF_PLATFORM]}] block", + [CONF_ESPHOME, CONF_PLATFORM], ) - config[CONF_ESPHOME] = config.pop("esphomeyaml") - core_key = "esphomeyaml" - if CONF_ESPHOME not in config: - raise cv.RequiredFieldInvalid("required key not provided", CONF_ESPHOME) - with cv.prepend_path(core_key): - out = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) - CORE.name = out[CONF_NAME] - CORE.esp_platform = out[CONF_PLATFORM] - with cv.prepend_path(core_key): - out2 = PRELOAD_CONFIG_SCHEMA2(config[CONF_ESPHOME]) - CORE.board = out2[CONF_BOARD] - CORE.build_path = CORE.relative_config_path(out2[CONF_BUILD_PATH]) + if len(newstyle_found) > 1: + raise cv.Invalid( + f"Found multiple target platform blocks: {', '.join(newstyle_found)}. Only one is allowed.", + [newstyle_found[0]], + ) + if newstyle_found: + # Convert to newstyle + for key in oldstyle_opts: + if key in conf: + raise cv.Invalid( + f"Please move {key} to the [{newstyle_found[0]}] block.", + [CONF_ESPHOME, key], + ) + + if has_oldstyle: + plat = conf.pop(CONF_PLATFORM) + plat_conf = {} + if CONF_ESP8266_RESTORE_FROM_FLASH in conf: + plat_conf["restore_from_flash"] = conf.pop(CONF_ESP8266_RESTORE_FROM_FLASH) + if CONF_BOARD_FLASH_MODE in conf: + plat_conf[CONF_BOARD_FLASH_MODE] = conf.pop(CONF_BOARD_FLASH_MODE) + if CONF_ARDUINO_VERSION in conf: + plat_conf[CONF_FRAMEWORK] = { + CONF_TYPE: "arduino", + CONF_VERSION: conf.pop(CONF_ARDUINO_VERSION), + } + if CONF_BOARD in conf: + plat_conf[CONF_BOARD] = conf.pop(CONF_BOARD) + # Insert generated target platform config to main config + config[plat] = plat_conf + config[CONF_ESPHOME] = conf def include_file(path, basename): @@ -274,25 +220,10 @@ async def add_includes(includes): @coroutine_with_priority(-1000.0) -async def _esp8266_add_lwip_type(): - # If any component has already set this, do not change it - if any( - flag.startswith("-DPIO_FRAMEWORK_ARDUINO_LWIP2_") for flag in CORE.build_flags - ): - return - - # Default for platformio is LWIP2_LOW_MEMORY with: - # - MSS=536 - # - LWIP_FEATURES enabled - # - this only adds some optional features like IP incoming packet reassembly and NAPT - # see also: - # https://github.com/esp8266/Arduino/blob/master/tools/sdk/lwip2/include/lwipopts.h - - # Instead we use LWIP2_HIGHER_BANDWIDTH_LOW_FLASH with: - # - MSS=1460 - # - LWIP_FEATURES disabled (because we don't need them) - # Other projects like Tasmota & ESPEasy also use this - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH") +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(): + cg.add_platformio_option(key, val) @coroutine_with_priority(30.0) @@ -316,6 +247,11 @@ async def _add_automations(config): @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(cg.global_ns.namespace("esphome").using) + # These can be used by user lambdas, put them to default scope + cg.add_global(cg.RawExpression("using std::isnan")) + cg.add_global(cg.RawExpression("using std::min")) + cg.add_global(cg.RawExpression("using std::max")) + cg.add( cg.App.pre_setup( config[CONF_NAME], @@ -326,10 +262,6 @@ async def to_code(config): CORE.add_job(_add_automations, config) - # Set LWIP build constants for ESP8266 - if CORE.is_esp8266: - CORE.add_job(_esp8266_add_lwip_type) - cg.add_build_flag("-fno-exceptions") # Libraries @@ -337,19 +269,27 @@ async def to_code(config): if "@" in lib: name, vers = lib.split("@", 1) cg.add_library(name, vers) + elif "://" in lib: + # Repository... + if "=" in lib: + name, repo = lib.split("=", 1) + cg.add_library(name, None, repo) + else: + cg.add_library(None, None, lib) + else: cg.add_library(lib, None) cg.add_build_flag("-Wno-unused-variable") cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-sign-compare") - if config.get(CONF_ESP8266_RESTORE_FROM_FLASH, False): - cg.add_define("USE_ESP8266_PREFERENCES_FLASH") if config[CONF_INCLUDES]: CORE.add_job(add_includes, config[CONF_INCLUDES]) - cg.add_define("ESPHOME_BOARD", CORE.board) 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 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 f3d10b23ff..1d25be41f2 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -53,6 +53,18 @@ void Controller::setup_controller() { obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); }); } #endif +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); + } +#endif +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0e94a43c4c..0de8f7ea19 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -25,6 +25,12 @@ #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { @@ -55,6 +61,12 @@ class Controller { #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif +#ifdef USE_NUMBER + virtual void on_number_update(number::Number *obj, float state){}; +#endif +#ifdef USE_SELECT + virtual void on_select_update(select::Select *obj, const std::string &state){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 90562510b9..7c2261920a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -1,29 +1,72 @@ #pragma once -// This file is auto-generated! Do not edit! -#define USE_API -#define USE_LOGGER -#define USE_BINARY_SENSOR -#define USE_SENSOR -#define USE_SWITCH -#define USE_WIFI -#define USE_STATUS_LED -#define USE_TEXT_SENSOR -#define USE_FAN -#define USE_COVER -#define USE_LIGHT -#define USE_CLIMATE -#define USE_MQTT -#define USE_POWER_SUPPLY -#define USE_HOMEASSISTANT_TIME -#define USE_JSON -#ifdef ARDUINO_ARCH_ESP32 -#define USE_ESP32_CAMERA -#define USE_ESP32_BLE_SERVER -#define USE_IMPROV -#endif -#define USE_TIME -#define USE_DEEP_SLEEP -#define USE_CAPTIVE_PORTAL +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the relevant feature flags for the current build. +// +// This file is only used by static analyzers and IDEs. + +// Informative flags #define ESPHOME_BOARD "dummy_board" +#define ESPHOME_PROJECT_NAME "dummy project" +#define ESPHOME_PROJECT_VERSION "v2" + +// Feature flags +#define USE_API +#define USE_API_NOISE +#define USE_API_PLAINTEXT +#define USE_BINARY_SENSOR +#define USE_CLIMATE +#define USE_COVER +#define USE_DEEP_SLEEP +#define USE_ESP8266_PREFERENCES_FLASH +#define USE_FAN +#define USE_GRAPH +#define USE_HOMEASSISTANT_TIME +#define USE_LIGHT +#define USE_LOGGER #define USE_MDNS +#define USE_NUMBER +#define USE_OTA_STATE_CALLBACK +#define USE_POWER_SUPPLY +#define USE_PROMETHEUS +#define USE_SELECT +#define USE_SENSOR +#define USE_STATUS_LED +#define USE_SWITCH +#define USE_TEXT_SENSOR +#define USE_TIME +#define USE_WIFI + +// Arduino-specific feature flags +#ifdef USE_ARDUINO +#define USE_CAPTIVE_PORTAL +#define USE_JSON +#define USE_NEXTION_TFT_UPLOAD +#define USE_MQTT +#define USE_WIFI_WPA2_EAP +#endif + +// ESP32-specific feature flags +#ifdef USE_ESP32 +#define USE_ESP32_BLE_SERVER +#define USE_ESP32_CAMERA +#define USE_ESP32_IGNORE_EFUSE_MAC_CRC +#define USE_IMPROV +#define USE_SOCKET_IMPL_BSD_SOCKETS + +#ifdef USE_ARDUINO +#define USE_ETHERNET +#endif +#endif + +// ESP8266-specific feature flags +#ifdef USE_ESP8266 +#define USE_ADC_SENSOR_VCC +#define USE_HTTP_REQUEST_ESP8266_HTTPS +#define USE_SOCKET_IMPL_LWIP_TCP +#endif + +// Disabled feature flags +//#define USE_BSEC // Requires a library with proprietary license. + +#define USE_DASHBOARD_IMPORT diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp new file mode 100644 index 0000000000..bc94da85fe --- /dev/null +++ b/esphome/core/entity_base.cpp @@ -0,0 +1,40 @@ +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { + +static const char *const TAG = "entity_base"; + +EntityBase::EntityBase(std::string name) : name_(std::move(name)) { this->calc_object_id_(); } + +// Entity Name +const std::string &EntityBase::get_name() const { return this->name_; } +void EntityBase::set_name(const std::string &name) { + this->name_ = name; + this->calc_object_id_(); +} + +// Entity Internal +bool EntityBase::is_internal() const { return this->internal_; } +void EntityBase::set_internal(bool internal) { this->internal_ = internal; } + +// Entity Disabled by Default +bool EntityBase::is_disabled_by_default() const { return this->disabled_by_default_; } +void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } + +// Entity Icon +const std::string &EntityBase::get_icon() const { return this->icon_; } +void EntityBase::set_icon(const std::string &name) { this->icon_ = name; } + +// Entity Object ID +const std::string &EntityBase::get_object_id() { return this->object_id_; } + +// Calculate Object ID Hash from Entity Name +void EntityBase::calc_object_id_() { + this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST); + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(this->object_id_); +} +uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } + +} // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h new file mode 100644 index 0000000000..263747b721 --- /dev/null +++ b/esphome/core/entity_base.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +namespace esphome { + +// The generic Entity base class that provides an interface common to all Entities. +class EntityBase { + public: + EntityBase() : EntityBase("") {} + explicit EntityBase(std::string name); + + // Get/set the name of this Entity + const std::string &get_name() const; + void set_name(const std::string &name); + + // Get the sanitized name of this Entity as an ID. Caching it internally. + const std::string &get_object_id(); + + // Get the unique Object ID of this Entity + uint32_t get_object_id_hash(); + + // Get/set whether this Entity should be hidden from outside of ESPHome + bool is_internal() const; + void set_internal(bool internal); + + // Check if this object is declared to be disabled by default. + // That means that when the device gets added to Home Assistant (or other clients) it should + // not be added to the default view by default, and a user action is necessary to manually add it. + bool is_disabled_by_default() const; + void set_disabled_by_default(bool disabled_by_default); + + // Get/set this entity's icon + const std::string &get_icon() const; + void set_icon(const std::string &name); + + protected: + virtual uint32_t hash_base() = 0; + void calc_object_id_(); + + std::string name_; + std::string object_id_; + std::string icon_; + uint32_t object_id_hash_; + bool internal_{false}; + bool disabled_by_default_{false}; +}; + +} // namespace esphome diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py new file mode 100644 index 0000000000..b2dbe2116e --- /dev/null +++ b/esphome/core/entity_helpers.py @@ -0,0 +1,32 @@ +import esphome.final_validate as fv + +from esphome.const import CONF_ID + + +def inherit_property_from(property_to_inherit, parent_id_property): + """Validator that inherits a configuration property from another entity, for use with FINAL_VALIDATE_SCHEMA. + + If a property is already set, it will not be inherited. + + Keyword arguments: + property_to_inherit -- the name of the property to inherit, e.g. CONF_ICON + parent_id_property -- the name of the property that holds the ID of the parent, e.g. CONF_POWER_ID + """ + + def inherit_property(config): + if property_to_inherit not in config: + fconf = fv.full_config.get() + + # Get config for the parent entity + path = fconf.get_path_for_id(config[parent_id_property])[:-1] + parent_config = fconf.get_config_for_path(path) + + # If parent sensor has the property set, inherit it + if property_to_inherit in parent_config: + path = fconf.get_path_for_id(config[CONF_ID])[:-1] + this_config = fconf.get_config_for_path(path) + this_config[property_to_inherit] = parent_config[property_to_inherit] + + return config + + return inherit_property diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp deleted file mode 100644 index d7adc8dbf1..0000000000 --- a/esphome/core/esphal.cpp +++ /dev/null @@ -1,317 +0,0 @@ -#include "esphome/core/esphal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#ifdef ARDUINO_ARCH_ESP8266 -extern "C" { -typedef struct { // NOLINT - void *interruptInfo; // NOLINT - void *functionInfo; // NOLINT -} ArgStructure; - -void ICACHE_RAM_ATTR __attachInterruptArg(uint8_t pin, void (*)(void *), void *fp, // NOLINT - int mode); -void ICACHE_RAM_ATTR __detachInterrupt(uint8_t pin); // NOLINT -}; -#endif - -namespace esphome { - -static const char *const TAG = "esphal"; - -GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted) - : pin_(pin), - mode_(mode), - inverted_(inverted), -#ifdef ARDUINO_ARCH_ESP8266 - gpio_read_(pin < 16 ? &GPI : &GP16I), - gpio_mask_(pin < 16 ? (1UL << pin) : 1) -#endif -#ifdef ARDUINO_ARCH_ESP32 - gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), - gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val), - gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val), - gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32))) -#endif -{ -} - -const char *GPIOPin::get_pin_mode_name() const { - const char *mode_s; - switch (this->mode_) { - case INPUT: - mode_s = "INPUT"; - break; - case OUTPUT: - mode_s = "OUTPUT"; - break; - case INPUT_PULLUP: - mode_s = "INPUT_PULLUP"; - break; - case OUTPUT_OPEN_DRAIN: - mode_s = "OUTPUT_OPEN_DRAIN"; - break; - case SPECIAL: - mode_s = "SPECIAL"; - break; - case FUNCTION_1: - mode_s = "FUNCTION_1"; - break; - case FUNCTION_2: - mode_s = "FUNCTION_2"; - break; - case FUNCTION_3: - mode_s = "FUNCTION_3"; - break; - case FUNCTION_4: - mode_s = "FUNCTION_4"; - break; - -#ifdef ARDUINO_ARCH_ESP32 - case PULLUP: - mode_s = "PULLUP"; - break; - case PULLDOWN: - mode_s = "PULLDOWN"; - break; - case INPUT_PULLDOWN: - mode_s = "INPUT_PULLDOWN"; - break; - case OPEN_DRAIN: - mode_s = "OPEN_DRAIN"; - break; - case FUNCTION_5: - mode_s = "FUNCTION_5"; - break; - case FUNCTION_6: - mode_s = "FUNCTION_6"; - break; - case ANALOG: - mode_s = "ANALOG"; - break; -#endif -#ifdef ARDUINO_ARCH_ESP8266 - case FUNCTION_0: - mode_s = "FUNCTION_0"; - break; - case WAKEUP_PULLUP: - mode_s = "WAKEUP_PULLUP"; - break; - case WAKEUP_PULLDOWN: - mode_s = "WAKEUP_PULLDOWN"; - break; - case INPUT_PULLDOWN_16: - mode_s = "INPUT_PULLDOWN_16"; - break; -#endif - - default: - mode_s = "UNKNOWN"; - break; - } - - return mode_s; -} - -unsigned char GPIOPin::get_pin() const { return this->pin_; } -unsigned char GPIOPin::get_mode() const { return this->mode_; } - -bool GPIOPin::is_inverted() const { return this->inverted_; } -void GPIOPin::setup() { this->pin_mode(this->mode_); } -bool ICACHE_RAM_ATTR HOT GPIOPin::digital_read() { - return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; -} -bool ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_read() { - return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; -} -void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) { -#ifdef ARDUINO_ARCH_ESP8266 - if (this->pin_ != 16) { - if (value != this->inverted_) { - GPOS = this->gpio_mask_; - } else { - GPOC = this->gpio_mask_; - } - } else { - if (value != this->inverted_) { - GP16O |= 1; - } else { - GP16O &= ~1; - } - } -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (value != this->inverted_) { - (*this->gpio_set_) = this->gpio_mask_; - } else { - (*this->gpio_clear_) = this->gpio_mask_; - } -#endif -} -void ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_write(bool value) { -#ifdef ARDUINO_ARCH_ESP8266 - if (this->pin_ != 16) { - if (value != this->inverted_) { - GPOS = this->gpio_mask_; - } else { - GPOC = this->gpio_mask_; - } - } else { - if (value != this->inverted_) { - GP16O |= 1; - } else { - GP16O &= ~1; - } - } -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (value != this->inverted_) { - (*this->gpio_set_) = this->gpio_mask_; - } else { - (*this->gpio_clear_) = this->gpio_mask_; - } -#endif -} -ISRInternalGPIOPin::ISRInternalGPIOPin(uint8_t pin, -#ifdef ARDUINO_ARCH_ESP32 - volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, -#endif - volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted) - : pin_(pin), - inverted_(inverted), - gpio_read_(gpio_read), - gpio_mask_(gpio_mask) -#ifdef ARDUINO_ARCH_ESP32 - , - gpio_clear_(gpio_clear), - gpio_set_(gpio_set) -#endif -{ -} -void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { -#ifdef ARDUINO_ARCH_ESP8266 - GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_); -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (this->pin_ < 32) { - GPIO.status_w1tc = this->gpio_mask_; - } else { - GPIO.status1_w1tc.intr_st = this->gpio_mask_; - } -#endif -} - -void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { -#ifdef ARDUINO_ARCH_ESP8266 - if (this->pin_ == 16 && mode == INPUT_PULLUP) { - // pullups are not available on GPIO16, manually override with - // input mode. - pinMode(16, INPUT); - return; - } -#endif - pinMode(this->pin_, mode); -} - -#ifdef ARDUINO_ARCH_ESP8266 -struct ESPHomeInterruptFuncInfo { - void (*func)(void *); - void *arg; -}; - -void ICACHE_RAM_ATTR interrupt_handler(void *arg) { - ArgStructure *as = static_cast(arg); - auto *info = static_cast(as->functionInfo); - info->func(info->arg); -} -#endif - -void GPIOPin::detach_interrupt() const { this->detach_interrupt_(); } -void GPIOPin::detach_interrupt_() const { -#ifdef ARDUINO_ARCH_ESP8266 - __detachInterrupt(get_pin()); -#endif -#ifdef ARDUINO_ARCH_ESP32 - detachInterrupt(get_pin()); -#endif -} -void GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, int mode) const { - if (this->inverted_) { - if (mode == RISING) { - mode = FALLING; - } else if (mode == FALLING) { - mode = RISING; - } - } -#ifdef ARDUINO_ARCH_ESP8266 - ArgStructure *as = new ArgStructure; - as->interruptInfo = nullptr; - - as->functionInfo = new ESPHomeInterruptFuncInfo{ - .func = func, - .arg = arg, - }; - - __attachInterruptArg(this->pin_, interrupt_handler, as, mode); -#endif -#ifdef ARDUINO_ARCH_ESP32 - // work around issue https://github.com/espressif/arduino-esp32/pull/1776 in arduino core - // yet again proves how horrible code is there :( - how could that have been accepted... - auto *attach = reinterpret_cast(attachInterruptArg); - attach(this->pin_, func, arg, mode); -#endif -} - -ISRInternalGPIOPin *GPIOPin::to_isr() const { - return new ISRInternalGPIOPin(this->pin_, -#ifdef ARDUINO_ARCH_ESP32 - this->gpio_clear_, this->gpio_set_, -#endif - this->gpio_read_, this->gpio_mask_, this->inverted_); -} - -void force_link_symbols() { -#ifdef ARDUINO_ARCH_ESP8266 - // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible - // with their settings - ESPHome uses a different settings system (that can also survive - // erases). So set magic bytes indicating all tasmota versions are supported. - // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support - // for Tasmota. - // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380 - // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654 - const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A}; - // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO) - volatile int x = 0; - x = TASMOTA_MAGIC_BYTES[x]; -#endif -} - -} // namespace esphome - -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -// Fix 2.3.0 std missing memchr -extern "C" { -void *memchr(const void *s, int c, size_t n) { - if (n == 0) - return nullptr; - const uint8_t *p = reinterpret_cast(s); - do { - if (*p++ == c) - return const_cast(reinterpret_cast(p - 1)); - } while (--n != 0); - return nullptr; -} -}; -#endif - -#ifdef ARDUINO_ARCH_ESP8266 -extern "C" { -extern void resetPins() { // NOLINT - // Added in framework 2.7.0 - // usually this sets up all pins to be in INPUT mode - // however, not strictly needed as we set up the pins properly - // ourselves and this causes pins to toggle during reboot. -} -} -#endif diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h deleted file mode 100644 index 809c7d91b5..0000000000 --- a/esphome/core/esphal.h +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include "Arduino.h" -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -// Fix some arduino defs -#ifdef round -#undef round -#endif -#ifdef bool -#undef bool -#endif -#ifdef true -#undef true -#endif -#ifdef false -#undef false -#endif -#ifdef min -#undef min -#endif -#ifdef max -#undef max -#endif -#ifdef abs -#undef abs -#endif - -namespace esphome { - -#define LOG_PIN(prefix, pin) \ - if ((pin) != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix LOG_PIN_PATTERN, LOG_PIN_ARGS(pin)); \ - } -#define LOG_PIN_PATTERN "GPIO%u (Mode: %s%s)" -#define LOG_PIN_ARGS(pin) (pin)->get_pin(), (pin)->get_pin_mode_name(), ((pin)->is_inverted() ? ", INVERTED" : "") - -/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) -class ISRInternalGPIOPin { - public: - ISRInternalGPIOPin(uint8_t pin, -#ifdef ARDUINO_ARCH_ESP32 - volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, -#endif - volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted); - bool digital_read(); - void digital_write(bool value); - void clear_interrupt(); - - protected: - const uint8_t pin_; - const bool inverted_; - volatile uint32_t *const gpio_read_; - const uint32_t gpio_mask_; -#ifdef ARDUINO_ARCH_ESP32 - volatile uint32_t *const gpio_clear_; - volatile uint32_t *const gpio_set_; -#endif -}; - -/** A high-level abstraction class that can expose a pin together with useful options like pinMode. - * - * Set the parameters for this at construction time and use setup() to apply them. The inverted parameter will - * automatically invert the input/output for you. - * - * Use read_value() and write_value() to use digitalRead() and digitalWrite(), respectively. - */ -class GPIOPin { - public: - /** Construct the GPIOPin instance. - * - * @param pin The GPIO pin number of this instance. - * @param mode The Arduino pinMode that this pin should be put into at setup(). - * @param inverted Whether all digitalRead/digitalWrite calls should be inverted. - */ - GPIOPin(uint8_t pin, uint8_t mode, bool inverted = false); - - /// Setup the pin mode. - virtual void setup(); - /// Read the binary value from this pin using digitalRead (and inverts automatically). - virtual bool digital_read(); - /// Write the binary value to this pin using digitalWrite (and inverts automatically). - virtual void digital_write(bool value); - /// Set the pin mode - virtual void pin_mode(uint8_t mode); - - /// Get the GPIO pin number. - uint8_t get_pin() const; - const char *get_pin_mode_name() const; - /// Get the pinMode of this pin. - uint8_t get_mode() const; - /// Return whether this pin shall be treated as inverted. (for example active-low) - bool is_inverted() const; - - template void attach_interrupt(void (*func)(T *), T *arg, int mode) const; - void detach_interrupt() const; - - ISRInternalGPIOPin *to_isr() const; - - protected: - void attach_interrupt_(void (*func)(void *), void *arg, int mode) const; - void detach_interrupt_() const; - - const uint8_t pin_; - const uint8_t mode_; - const bool inverted_; -#ifdef ARDUINO_ARCH_ESP32 - volatile uint32_t *const gpio_set_; - volatile uint32_t *const gpio_clear_; -#endif - volatile uint32_t *const gpio_read_; - const uint32_t gpio_mask_; -}; - -template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { - this->attach_interrupt_(reinterpret_cast(func), arg, mode); -} -/** This function can be used by the HAL to force-link specific symbols - * into the generated binary without modifying the linker script. - * - * It is called by the application very early on startup and should not be used for anything - * other than forcing symbols to be linked. - */ -void force_link_symbols(); - -} // namespace esphome diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h new file mode 100644 index 0000000000..1d3fb89805 --- /dev/null +++ b/esphome/core/gpio.h @@ -0,0 +1,98 @@ +#pragma once +#include +#include + +namespace esphome { + +#define LOG_PIN(prefix, pin) \ + if ((pin) != nullptr) { \ + ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \ + } + +// put GPIO flags in a namepsace to not pollute esphome namespace +namespace gpio { + +enum Flags : uint8_t { + // Can't name these just INPUT because of Arduino defines :( + FLAG_NONE = 0x00, + FLAG_INPUT = 0x01, + FLAG_OUTPUT = 0x02, + FLAG_OPEN_DRAIN = 0x04, + FLAG_PULLUP = 0x08, + FLAG_PULLDOWN = 0x10, +}; + +class FlagsHelper { + public: + constexpr FlagsHelper(Flags val) : val_(val) {} + constexpr operator Flags() const { return val_; } + + protected: + Flags val_; +}; +constexpr FlagsHelper operator&(Flags lhs, Flags rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr FlagsHelper operator|(Flags lhs, Flags rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +enum InterruptType : uint8_t { + INTERRUPT_RISING_EDGE = 1, + INTERRUPT_FALLING_EDGE = 2, + INTERRUPT_ANY_EDGE = 3, + INTERRUPT_LOW_LEVEL = 4, + INTERRUPT_HIGH_LEVEL = 5, +}; + +} // namespace gpio + +class GPIOPin { + public: + virtual void setup() = 0; + + virtual void pin_mode(gpio::Flags flags) = 0; + + virtual bool digital_read() = 0; + + virtual void digital_write(bool value) = 0; + + virtual std::string dump_summary() const = 0; + + virtual bool is_internal() { return false; } +}; + +/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) +class ISRInternalGPIOPin { + public: + ISRInternalGPIOPin() = default; + ISRInternalGPIOPin(void *arg) : arg_(arg) {} + bool digital_read(); + void digital_write(bool value); + void clear_interrupt(); + + protected: + void *arg_ = nullptr; +}; + +class InternalGPIOPin : public GPIOPin { + public: + template void attach_interrupt(void (*func)(T *), T *arg, gpio::InterruptType type) const { + this->attach_interrupt(reinterpret_cast(func), arg, type); + } + + virtual void detach_interrupt() const = 0; + + virtual ISRInternalGPIOPin to_isr() const = 0; + + virtual uint8_t get_pin() const = 0; + + bool is_internal() override { return true; } + + virtual bool is_inverted() const = 0; + + protected: + virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0; +}; + +} // namespace esphome diff --git a/esphome/core/hal.h b/esphome/core/hal.h new file mode 100644 index 0000000000..a86dbf2534 --- /dev/null +++ b/esphome/core/hal.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include +#include "gpio.h" + +#if defined(USE_ESP32_FRAMEWORK_ESP_IDF) +#include +#ifndef PROGMEM +#define PROGMEM +#endif + +#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) + +#include + +#ifndef PROGMEM +#define PROGMEM +#endif + +#elif defined(USE_ESP8266) + +#include +#ifndef PROGMEM +#define PROGMEM ICACHE_RODATA_ATTR +#endif + +#else + +#define IRAM_ATTR +#define PROGMEM + +#endif + +namespace esphome { + +void yield(); +uint32_t millis(); +uint32_t micros(); +void delay(uint32_t ms); +void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void __attribute__((noreturn)) arch_restart(); +void arch_feed_wdt(); +uint32_t arch_get_cpu_cycle_count(); +uint32_t arch_get_cpu_freq_hz(); +uint8_t progmem_read_byte(const uint8_t *addr); + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f78dcf3183..780df3ca6d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -1,29 +1,53 @@ #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" #include #include +#include +#include -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(USE_ESP8266) #include -#else +#include +#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) #include +#elif defined(USE_ESP_IDF) +#include "esp_system.h" +#include +#include +#endif +#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC +#include "esp_efuse.h" +#include "esp_efuse_table.h" #endif #include "esphome/core/log.h" -#include "esphome/core/esphal.h" +#include "esphome/core/hal.h" namespace esphome { static const char *const TAG = "helpers"; +void get_mac_address_raw(uint8_t *mac) { +#ifdef USE_ESP32 +#ifdef 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); +#else + esp_efuse_mac_get_default(mac); +#endif +#endif +#ifdef USE_ESP8266 + WiFi.macAddress(mac); +#endif +} + std::string get_mac_address() { char tmp[20]; uint8_t mac[6]; -#ifdef ARDUINO_ARCH_ESP32 - esp_efuse_mac_get_default(mac); -#endif -#ifdef ARDUINO_ARCH_ESP8266 - WiFi.macAddress(mac); -#endif + get_mac_address_raw(mac); sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(tmp); } @@ -31,22 +55,21 @@ std::string get_mac_address() { std::string get_mac_address_pretty() { char tmp[20]; uint8_t mac[6]; -#ifdef ARDUINO_ARCH_ESP32 - esp_efuse_mac_get_default(mac); -#endif -#ifdef ARDUINO_ARCH_ESP8266 - WiFi.macAddress(mac); -#endif + get_mac_address_raw(mac); sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(tmp); } +#ifdef USE_ESP32 +void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } +#endif + std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); } uint32_t random_uint32() { -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32 return esp_random(); -#else +#elif defined(USE_ESP8266) return os_random(); #endif } @@ -55,6 +78,17 @@ double random_double() { return random_uint32() / double(UINT32_MAX); } float random_float() { return float(random_double()); } +void fill_random(uint8_t *data, size_t len) { +#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) + esp_fill_random(data, len); +#elif defined(USE_ESP8266) + int err = os_get_random(data, len); + assert(err == 0); +#else +#error "No random source for this system config" +#endif +} + static uint32_t fast_random_seed = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } @@ -79,6 +113,15 @@ float gamma_correct(float value, float gamma) { return powf(value, gamma); } +float gamma_uncorrect(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, 1 / gamma); +} + std::string to_lowercase_underscore(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::replace(s.begin(), s.end(), ' ', '_'); @@ -105,10 +148,13 @@ std::string truncate_string(const std::string &s, size_t length) { } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - auto multiplier = float(pow10(accuracy_decimals)); - float value_rounded = roundf(value * multiplier) / multiplier; + if (accuracy_decimals < 0) { + auto multiplier = powf(10.0f, accuracy_decimals); + value = roundf(value * multiplier) / multiplier; + accuracy_decimals = 0; + } char tmp[32]; // should be enough, but we should maybe improve this at some point. - dtostrf(value_rounded, 0, uint8_t(std::max(0, int(accuracy_decimals))), tmp); + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } std::string uint64_to_string(uint64_t num) { @@ -123,21 +169,6 @@ std::string uint32_to_string(uint32_t num) { snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]); return std::string(buffer); } -static char *global_json_build_buffer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static size_t global_json_build_buffer_size = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void reserve_global_json_build_buffer(size_t required_size) { - if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) { - delete[] global_json_build_buffer; - global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2); - - size_t remainder = global_json_build_buffer_size % 16U; - if (remainder != 0) - global_json_build_buffer_size += 16 - remainder; - - global_json_build_buffer = new char[global_json_build_buffer_size]; - } -} ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && strcasecmp(str, "on") == 0) @@ -255,6 +286,39 @@ optional parse_int(const std::string &str) { return {}; return value; } + +optional parse_hex(const char chr) { + int out = chr; + if (out >= '0' && out <= '9') + return (out - '0'); + if (out >= 'A' && out <= 'F') + return (10 + (out - 'A')); + if (out >= 'a' && out <= 'f') + return (10 + (out - 'a')); + return {}; +} + +optional parse_hex(const std::string &str, size_t start, size_t length) { + if (str.length() < start) { + return {}; + } + size_t end = start + length; + if (str.length() < end) { + return {}; + } + int out = 0; + for (size_t i = start; i < end; i++) { + char chr = str[i]; + auto digit = parse_hex(chr); + if (!digit.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number, invalid character %c!", str.substr(start, length).c_str(), chr); + return {}; + } + out = (out << 4) | *digit; + } + return out; +} + uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { @@ -287,19 +351,37 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } -float clamp(float val, float min, float max) { +template T clamp(const T val, const T min, const T max) { if (val < min) return min; if (val > max) return max; return val; } +template float clamp(float, float, float); +template int clamp(int, int, int); + float lerp(float completion, float start, float end) { return start + (end - start) * completion; } bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; } bool str_endswith(const std::string &full, const std::string &ending) { return full.rfind(ending) == (full.size() - ending.size()); } +std::string str_sprintf(const char *fmt, ...) { + std::string str; + va_list args; + + va_start(args, fmt); + size_t length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + str.resize(length); + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + return str; +} uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); } std::array decode_uint16(uint16_t value) { @@ -328,13 +410,76 @@ std::string hexencode(const uint8_t *data, uint32_t len) { return res; } -#ifdef ARDUINO_ARCH_ESP8266 -ICACHE_RAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } -ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } +void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { + float max_color_value = std::max(std::max(red, green), blue); + float min_color_value = std::min(std::min(red, green), blue); + float delta = max_color_value - min_color_value; + + if (delta == 0) + hue = 0; + else if (max_color_value == red) + hue = int(fmod(((60 * ((green - blue) / delta)) + 360), 360)); + else if (max_color_value == green) + hue = int(fmod(((60 * ((blue - red) / delta)) + 120), 360)); + else if (max_color_value == blue) + hue = int(fmod(((60 * ((red - green) / delta)) + 240), 360)); + + if (max_color_value == 0) + saturation = 0; + else + saturation = delta / max_color_value; + + value = max_color_value; +} + +void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue) { + float chroma = value * saturation; + float hue_prime = fmod(hue / 60.0, 6); + float intermediate = chroma * (1 - fabs(fmod(hue_prime, 2) - 1)); + float delta = value - chroma; + + if (0 <= hue_prime && hue_prime < 1) { + red = chroma; + green = intermediate; + blue = 0; + } else if (1 <= hue_prime && hue_prime < 2) { + red = intermediate; + green = chroma; + blue = 0; + } else if (2 <= hue_prime && hue_prime < 3) { + red = 0; + green = chroma; + blue = intermediate; + } else if (3 <= hue_prime && hue_prime < 4) { + red = 0; + green = intermediate; + blue = chroma; + } else if (4 <= hue_prime && hue_prime < 5) { + red = intermediate; + green = 0; + blue = chroma; + } else if (5 <= hue_prime && hue_prime < 6) { + red = chroma; + green = 0; + blue = intermediate; + } else { + red = 0; + green = 0; + blue = 0; + } + + red += delta; + green += delta; + blue += delta; +} + +#ifdef USE_ESP8266 +IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } +IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } #endif -#ifdef ARDUINO_ARCH_ESP32 -ICACHE_RAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } -ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } +#ifdef USE_ESP32 +IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } +IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } #endif } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8096228a0f..61cc9a9e4a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -6,18 +6,14 @@ #include #include -#include "esphome/core/optional.h" -#include "esphome/core/esphal.h" - -#ifdef CLANG_TIDY -#undef ICACHE_RAM_ATTR -#define ICACHE_RAM_ATTR -#undef ICACHE_RODATA_ATTR -#define ICACHE_RODATA_ATTR +#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include "esp32-hal-psram.h" #endif +#include "esphome/core/optional.h" + #define HOT __attribute__((hot)) -#define ESPDEPRECATED(msg) __attribute__((deprecated(msg))) +#define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg))) #define ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) @@ -30,11 +26,21 @@ namespace esphome { /// The characters that are allowed in a hostname. extern const char *const HOSTNAME_CHARACTER_ALLOWLIST; -/// Gets the MAC address as a string, this can be used as way to identify this ESP. +/// Read the raw MAC address into the provided byte array (6 bytes). +void get_mac_address_raw(uint8_t *mac); + +/// Get the MAC address as a string, using lower case hex notation. +/// This can be used as way to identify this ESP. std::string get_mac_address(); +/// Get the MAC address as a string, using colon-separated upper case hex notation. std::string get_mac_address_pretty(); +#ifdef USE_ESP32 +/// Set the MAC address to use from the provided byte array (6 bytes). +void set_mac_address(uint8_t *mac); +#endif + std::string to_string(const std::string &val); std::string to_string(int val); std::string to_string(long val); // NOLINT @@ -47,7 +53,8 @@ std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); optional parse_int(const std::string &str); - +optional parse_hex(const std::string &str, size_t start, size_t length); +optional parse_hex(char chr); /// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname); @@ -62,6 +69,9 @@ bool str_equals_case_insensitive(const std::string &a, const std::string &b); bool str_startswith(const std::string &full, const std::string &start); bool str_endswith(const std::string &full, const std::string &ending); +/// sprintf-like function returning std::string instead of writing to char array. +std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); + class HighFrequencyLoopRequester { public: void start(); @@ -80,7 +90,7 @@ class HighFrequencyLoopRequester { * @param max The maximum value. * @return val clamped in between min and max. */ -float clamp(float val, float min, float max); +template T clamp(T val, T min, T max); /** Linearly interpolate between end start and end by completion. * @@ -92,10 +102,15 @@ float clamp(float val, float min, float max); */ float lerp(float completion, float start, float end); -/// std::make_unique +// Not all platforms we support target C++14 yet, so we can't unconditionally use std::make_unique. Provide our own +// implementation if needed, and otherwise pull std::make_unique into scope so that we have a uniform API. +#if __cplusplus >= 201402L +using std::make_unique; +#else template std::unique_ptr make_unique(Args &&...args) { return std::unique_ptr(new T(std::forward(args)...)); } +#endif /// Return a random 32 bit unsigned integer. uint32_t random_uint32(); @@ -109,6 +124,8 @@ double random_double(); /// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. float random_float(); +void fill_random(uint8_t *data, size_t len); + void fast_random_set_seed(uint32_t seed); uint32_t fast_random_32(); uint16_t fast_random_16(); @@ -116,6 +133,8 @@ uint8_t fast_random_8(); /// Applies gamma correction with the provided gamma to value. float gamma_correct(float value, float gamma); +/// Reverts gamma correction with the provided gamma to value. +float gamma_uncorrect(float value, float gamma); /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); @@ -140,13 +159,18 @@ std::array decode_uint16(uint16_t value); /// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb); +/// Convert RGB floats (0-1) to hue (0-360) & saturation/value percentage (0-1) +void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value); +/// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1) +void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue); + /*** * An interrupt helper class. * * This behaves like std::lock_guard. As long as the value is visible in the current stack, all interrupts * (including flash reads) will be disabled. * - * Please note all functions called when the interrupt lock must be marked ICACHE_RAM_ATTR (loading code into + * Please note all functions called when the interrupt lock must be marked IRAM_ATTR (loading code into * instruction cache is done via interrupts; disabling interrupts prevents data not already in cache from being * pulled from flash). * @@ -168,7 +192,7 @@ class InterruptLock { ~InterruptLock(); protected: -#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266 uint32_t xt_state_; #endif }; @@ -272,8 +296,8 @@ template class TemplatableValue { LAMBDA, } type_; - T value_; - std::function f_; + T value_{}; + std::function f_{}; }; template class TemplatableStringValue : public TemplatableValue { @@ -324,14 +348,14 @@ uint32_t fnv1_hash(const std::string &str); template T *new_buffer(size_t length) { T *buffer; -#ifdef ARDUINO_ARCH_ESP32 +#ifdef USE_ESP32_FRAMEWORK_ARDUINO if (psramFound()) { buffer = (T *) ps_malloc(length); } else { - buffer = new T[length]; + buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) } #else - buffer = new T[length]; + buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) #endif return buffer; diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 9b49a4c6ba..424154d253 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -46,7 +46,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif -#ifdef ARDUINO_ARCH_ESP32 +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; diff --git a/esphome/core/log.h b/esphome/core/log.h index 0eec28101f..590ad26032 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -3,16 +3,23 @@ #include #include #include + #ifdef USE_STORE_LOG_STR_IN_FLASH #include "WString.h" #endif -// avoid esp-idf redefining our macros -#include "esphome/core/esphal.h" +#include "esphome/core/macros.h" -#ifdef ARDUINO_ARCH_ESP32 -#include "esp_err.h" +// Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#include +#include #endif +#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include +#endif + +#include "esphome/core/macros.h" namespace esphome { @@ -55,7 +62,7 @@ void esp_log_vprintf_(int level, const char *tag, int line, const char *format, #ifdef USE_STORE_LOG_STR_IN_FLASH void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif -#ifdef ARDUINO_ARCH_ESP32 +#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif @@ -162,4 +169,37 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define ONOFF(b) ((b) ? "ON" : "OFF") #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") +// Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper) +struct LogString; + +#ifdef USE_STORE_LOG_STR_IN_FLASH + +#include + +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0) +#define LOG_STR_ARG(s) ((PGM_P)(s)) +#else +// Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a +// local buffer first. String length is limited to 63 characters. +// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31 +#define LOG_STR_ARG(s) \ + ({ \ + char __buf[64]; \ + __buf[63] = '\0'; \ + strncpy_P(__buf, (PGM_P)(s), 63); \ + __buf; \ + }) +#endif + +#define LOG_STR(s) (reinterpret_cast(PSTR(s))) +#define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s)) + +#else // !USE_STORE_LOG_STR_IN_FLASH + +#define LOG_STR(s) (reinterpret_cast(s)) +#define LOG_STR_ARG(s) (reinterpret_cast(s)) +#define LOG_STR_LITERAL(s) (s) + +#endif + } // namespace esphome diff --git a/esphome/core/macros.h b/esphome/core/macros.h new file mode 100644 index 0000000000..b0027a276c --- /dev/null +++ b/esphome/core/macros.h @@ -0,0 +1,56 @@ +#pragma once + +#define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch)) + +#if defined(USE_ESP8266) + +#include +#if defined(ARDUINO_ESP8266_MAJOR) && defined(ARDUINO_ESP8266_MINOR) && defined(ARDUINO_ESP8266_REVISION) // v3.0.1+ +#define ARDUINO_VERSION_CODE VERSION_CODE(ARDUINO_ESP8266_MAJOR, ARDUINO_ESP8266_MINOR, ARDUINO_ESP8266_REVISION) +#elif ARDUINO_ESP8266_GIT_VER == 0xefb0341a // version defines were screwed up in v3.0.0 +#define ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_4) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 4) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_3_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 3, 0) +#else +#warning "Could not determine Arduino framework version, update esphome/core/macros.h!" +#endif + +#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) + +#if defined(IDF_VER) // identifies v2, needed since v1 doesn't have the esp_arduino_version.h header +#include +#define ARDUINO_VERSION_CODE \ + VERSION_CODE(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATH) +#else +#define ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 0) // there are no defines identifying minor/patch version +#endif + +#endif diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 7ace7b122a..5b96781e63 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -16,6 +16,8 @@ // // Modified by Otto Winter on 18.05.18 +#include + namespace esphome { // type for nullopt diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp deleted file mode 100644 index 68030a2d59..0000000000 --- a/esphome/core/preferences.cpp +++ /dev/null @@ -1,308 +0,0 @@ -#include "esphome/core/preferences.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" -#include "esphome/core/application.h" - -#ifdef ARDUINO_ARCH_ESP8266 -extern "C" { -#include "spi_flash.h" -} -#endif -#ifdef ARDUINO_ARCH_ESP32 -#include "nvs.h" -#include "nvs_flash.h" -#endif - -namespace esphome { - -static const char *const TAG = "preferences"; - -ESPPreferenceObject::ESPPreferenceObject() : offset_(0), length_words_(0), type_(0), data_(nullptr) {} -ESPPreferenceObject::ESPPreferenceObject(size_t offset, size_t length, uint32_t type) - : offset_(offset), length_words_(length), type_(type) { - this->data_ = new uint32_t[this->length_words_ + 1]; - for (uint32_t i = 0; i < this->length_words_ + 1; i++) - this->data_[i] = 0; -} -bool ESPPreferenceObject::load_() { - if (!this->is_initialized()) { - ESP_LOGV(TAG, "Load Pref Not initialized!"); - return false; - } - if (!this->load_internal_()) - return false; - - bool valid = this->data_[this->length_words_] == this->calculate_crc_(); - - ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT - YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); - return valid; -} -bool ESPPreferenceObject::save_() { - if (!this->is_initialized()) { - ESP_LOGV(TAG, "Save Pref Not initialized!"); - return false; - } - - this->data_[this->length_words_] = this->calculate_crc_(); - if (!this->save_internal_()) - return false; - ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT - this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); - return true; -} - -#ifdef ARDUINO_ARCH_ESP8266 - -static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; -#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; -static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; - -#ifdef USE_ESP8266_PREFERENCES_FLASH -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; -#else -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; -#endif - -static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { - if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { - return false; - } - *dest = ESP_RTC_USER_MEM[index]; - return true; -} - -static bool esp8266_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { - if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { - return false; - } - if (index < 32 && global_preferences.is_prevent_write()) { - return false; - } - - auto *ptr = &ESP_RTC_USER_MEM[index]; - *ptr = value; - return true; -} - -extern "C" uint32_t _SPIFFS_end; // NOLINT - -static const uint32_t get_esp8266_flash_sector() { - union { - uint32_t *ptr; - uint32_t uint; - } data{}; - data.ptr = &_SPIFFS_end; - return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE; -} -static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } - -void ESPPreferences::save_esp8266_flash_() { - if (!esp8266_flash_dirty) - return; - - ESP_LOGVV(TAG, "Saving preferences to flash..."); - SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; - { - InterruptLock lock; - erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); - if (erase_res == SPI_FLASH_RESULT_OK) { - write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); - } - } - if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); - return; - } - if (write_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Write ESP8266 flash failed!"); - return; - } - - esp8266_flash_dirty = false; -} - -bool ESPPreferenceObject::save_internal_() { - if (this->in_flash_) { - for (uint32_t i = 0; i <= this->length_words_; i++) { - uint32_t j = this->offset_ + i; - if (j >= ESP8266_FLASH_STORAGE_SIZE) - return false; - uint32_t v = this->data_[i]; - uint32_t *ptr = &global_preferences.flash_storage_[j]; - if (*ptr != v) - esp8266_flash_dirty = true; - *ptr = v; - } - global_preferences.save_esp8266_flash_(); - return true; - } - - for (uint32_t i = 0; i <= this->length_words_; i++) { - if (!esp_rtc_user_mem_write(this->offset_ + i, this->data_[i])) - return false; - } - - return true; -} -bool ESPPreferenceObject::load_internal_() { - if (this->in_flash_) { - for (uint32_t i = 0; i <= this->length_words_; i++) { - uint32_t j = this->offset_ + i; - if (j >= ESP8266_FLASH_STORAGE_SIZE) - return false; - this->data_[i] = global_preferences.flash_storage_[j]; - } - - return true; - } - - for (uint32_t i = 0; i <= this->length_words_; i++) { - if (!esp_rtc_user_mem_read(this->offset_ + i, &this->data_[i])) - return false; - } - return true; -} -ESPPreferences::ESPPreferences() - // offset starts from start of user RTC mem (64 words before that are reserved for system), - // an additional 32 words at the start of user RTC are for eboot (OTA, see eboot_command.h), - // which will be reset each time OTA occurs - : current_offset_(0) {} - -void ESPPreferences::begin() { - this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; - ESP_LOGVV(TAG, "Loading preferences from flash..."); - - { - InterruptLock lock; - spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); - } -} - -ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { - if (in_flash) { - uint32_t start = this->current_flash_offset_; - uint32_t end = start + length + 1; - if (end > ESP8266_FLASH_STORAGE_SIZE) - return {}; - auto pref = ESPPreferenceObject(start, length, type); - pref.in_flash_ = true; - this->current_flash_offset_ = end; - return pref; - } - - uint32_t start = this->current_offset_; - uint32_t end = start + length + 1; - bool in_normal = start < 96; - // Normal: offset 0-95 maps to RTC offset 32 - 127, - // Eboot: offset 96-127 maps to RTC offset 0 - 31 words - if (in_normal && end > 96) { - // start is in normal but end is not -> switch to Eboot - this->current_offset_ = start = 96; - end = start + length + 1; - in_normal = false; - } - - if (end > 128) { - // Doesn't fit in data, return uninitialized preference obj. - return {}; - } - - uint32_t rtc_offset; - if (in_normal) { - rtc_offset = start + 32; - } else { - rtc_offset = start - 96; - } - - auto pref = ESPPreferenceObject(rtc_offset, length, type); - this->current_offset_ += length + 1; - return pref; -} -void ESPPreferences::prevent_write(bool prevent) { this->prevent_write_ = prevent; } -bool ESPPreferences::is_prevent_write() { return this->prevent_write_; } -#endif - -#ifdef ARDUINO_ARCH_ESP32 -bool ESPPreferenceObject::save_internal_() { - if (global_preferences.nvs_handle_ == 0) - return false; - - char key[32]; - sprintf(key, "%u", this->offset_); - uint32_t len = (this->length_words_ + 1) * 4; - esp_err_t err = nvs_set_blob(global_preferences.nvs_handle_, key, this->data_, len); - if (err) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", key, len, esp_err_to_name(err)); - return false; - } - err = nvs_commit(global_preferences.nvs_handle_); - if (err) { - ESP_LOGV(TAG, "nvs_commit('%s', len=%u) failed: %s", key, len, esp_err_to_name(err)); - return false; - } - return true; -} -bool ESPPreferenceObject::load_internal_() { - if (global_preferences.nvs_handle_ == 0) - return false; - - char key[32]; - sprintf(key, "%u", this->offset_); - size_t len = (this->length_words_ + 1) * 4; - - size_t actual_len; - esp_err_t err = nvs_get_blob(global_preferences.nvs_handle_, key, nullptr, &actual_len); - if (err) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key, esp_err_to_name(err)); - return false; - } - if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match. Assuming key changed (%u!=%u)", actual_len, len); - return false; - } - err = nvs_get_blob(global_preferences.nvs_handle_, key, this->data_, &len); - if (err) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key, esp_err_to_name(err)); - return false; - } - return true; -} -ESPPreferences::ESPPreferences() : current_offset_(0) {} -void ESPPreferences::begin() { - auto ns = truncate_string(App.get_name(), 15); - esp_err_t err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_); - if (err) { - ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS...", esp_err_to_name(err)); - nvs_flash_deinit(); - nvs_flash_erase(); - nvs_flash_init(); - - err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_); - if (err) { - this->nvs_handle_ = 0; - } - } -} - -ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { - auto pref = ESPPreferenceObject(this->current_offset_, length, type); - this->current_offset_++; - return pref; -} -#endif -uint32_t ESPPreferenceObject::calculate_crc_() const { - uint32_t crc = this->type_; - for (size_t i = 0; i < this->length_words_; i++) { - crc ^= (this->data_[i] * 2654435769UL) >> 1; - } - return crc; -} -bool ESPPreferenceObject::is_initialized() const { return this->data_ != nullptr; } - -ESPPreferences global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -} // namespace esphome diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 43d0f023e3..ad45cd9684 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -1,109 +1,69 @@ #pragma once -#include - -#include "esphome/core/esphal.h" -#include "esphome/core/defines.h" +#include +#include +#include namespace esphome { -class ESPPreferenceObject { +class ESPPreferenceBackend { public: - ESPPreferenceObject(); - ESPPreferenceObject(size_t offset, size_t length, uint32_t type); - - template bool save(T *src); - - template bool load(T *dest); - - bool is_initialized() const; - - protected: - friend class ESPPreferences; - - bool save_(); - bool load_(); - bool save_internal_(); - bool load_internal_(); - - uint32_t calculate_crc_() const; - - size_t offset_; - size_t length_words_; - uint32_t type_; - uint32_t *data_; -#ifdef ARDUINO_ARCH_ESP8266 - bool in_flash_{false}; -#endif + virtual bool save(const uint8_t *data, size_t len) = 0; + virtual bool load(uint8_t *data, size_t len) = 0; }; -#ifdef ARDUINO_ARCH_ESP8266 -#ifdef USE_ESP8266_PREFERENCES_FLASH -static const bool DEFAULT_IN_FLASH = true; -#else -static const bool DEFAULT_IN_FLASH = false; -#endif -#endif +class ESPPreferenceObject { + public: + ESPPreferenceObject() = default; + ESPPreferenceObject(ESPPreferenceBackend *backend) : backend_(backend) {} -#ifdef ARDUINO_ARCH_ESP32 -static const bool DEFAULT_IN_FLASH = true; -#endif + template bool save(const T *src) { + if (backend_ == nullptr) + return false; + return backend_->save(reinterpret_cast(src), sizeof(T)); + } + + template bool load(T *dest) { + if (backend_ == nullptr) + return false; + return backend_->load(reinterpret_cast(dest), sizeof(T)); + } + + protected: + ESPPreferenceBackend *backend_{nullptr}; +}; class ESPPreferences { public: - ESPPreferences(); - void begin(); - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash = DEFAULT_IN_FLASH); - template ESPPreferenceObject make_preference(uint32_t type, bool in_flash = DEFAULT_IN_FLASH); + virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) = 0; + virtual ESPPreferenceObject make_preference(size_t length, uint32_t type) = 0; -#ifdef ARDUINO_ARCH_ESP8266 - /** On the ESP8266, we can't override the first 128 bytes during OTA uploads - * as the eboot parameters are stored there. Writing there during an OTA upload - * would invalidate applying the new firmware. During normal operation, we use - * this part of the RTC user memory, but stop writing to it during OTA uploads. + /** + * Commit pending writes to flash. * - * @param prevent Whether to prevent writing to the first 32 words of RTC user memory. + * @return true if write is successful. */ - void prevent_write(bool prevent); - bool is_prevent_write(); -#endif + virtual bool sync() = 0; - protected: - friend ESPPreferenceObject; - - uint32_t current_offset_; -#ifdef ARDUINO_ARCH_ESP32 - uint32_t nvs_handle_; +#ifndef USE_ESP8266 + template::value, bool>::type = true> +#else + // esp8266 toolchain doesn't have is_trivially_copyable + template #endif -#ifdef ARDUINO_ARCH_ESP8266 - void save_esp8266_flash_(); - bool prevent_write_{false}; - uint32_t *flash_storage_; - uint32_t current_flash_offset_; + ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { + return this->make_preference(sizeof(T), type, in_flash); + } +#ifndef USE_ESP8266 + template::value, bool>::type = true> +#else + template #endif + ESPPreferenceObject make_preference(uint32_t type) { + return this->make_preference(sizeof(T), type); + } }; -extern ESPPreferences global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -template ESPPreferenceObject ESPPreferences::make_preference(uint32_t type, bool in_flash) { - return this->make_preference((sizeof(T) + 3) / 4, type, in_flash); -} - -template bool ESPPreferenceObject::save(T *src) { - if (!this->is_initialized()) - return false; - memset(this->data_, 0, this->length_words_ * 4); - memcpy(this->data_, src, sizeof(T)); - return this->save_(); -} - -template bool ESPPreferenceObject::load(T *dest) { - memset(this->data_, 0, this->length_words_ * 4); - if (!this->load_()) - return false; - - memcpy(dest, this->data_, sizeof(T)); - return true; -} +extern ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 410c68052f..a6d3e0307e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -1,13 +1,13 @@ #include "scheduler.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" #include namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler @@ -82,7 +82,7 @@ optional HOT Scheduler::next_schedule_in() { return 0; return next_time - now; } -void ICACHE_RAM_ATTR HOT Scheduler::call() { +void IRAM_ATTR HOT Scheduler::call() { const uint32_t now = this->millis_(); this->process_to_add(); @@ -155,7 +155,10 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { // Warning: During f(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - item->f(); + { + WarnIfComponentBlockingGuard guard{item->component}; + item->f(); + } } { diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index 67e575e1c5..996cf8e310 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -4,47 +4,16 @@ #include "esphome/core/version.h" #include "esphome/core/log.h" -#ifdef USE_WIFI -#include "esphome/components/wifi/wifi_component.h" -#endif - #ifdef USE_API #include "esphome/components/api/api_server.h" #endif -#ifdef USE_ETHERNET -#include "esphome/components/ethernet/ethernet_component.h" -#endif - #ifdef USE_MQTT #include "esphome/components/mqtt/mqtt_client.h" #endif -#ifdef USE_MDNS -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif -#endif - namespace esphome { -bool network_is_connected() { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) - return true; -#endif - -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->is_connected(); -#endif - - return false; -} - bool api_is_connected() { #ifdef USE_API if (api::global_api_server != nullptr) { @@ -65,78 +34,4 @@ bool mqtt_is_connected() { bool remote_is_connected() { return api_is_connected() || mqtt_is_connected(); } -#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) -static bool mdns_setup; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#endif - -#ifndef WEBSERVER_PORT -static const uint8_t WEBSERVER_PORT = 80; -#endif - -#ifdef USE_MDNS -#ifdef ARDUINO_ARCH_ESP8266 -void network_setup_mdns(IPAddress address, int interface) { - // Latest arduino framework breaks mDNS for AP interface - // see https://github.com/esp8266/Arduino/issues/6114 - if (interface == 1) - return; - MDNS.begin(App.get_name().c_str(), std::move(address)); - mdns_setup = true; -#endif -#ifdef ARDUINO_ARCH_ESP32 - void network_setup_mdns() { - MDNS.begin(App.get_name().c_str()); -#endif -#ifdef USE_API - if (api::global_api_server != nullptr) { - MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port()); - // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version - MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); - MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); - MDNS.addServiceTxt("esphomelib", "tcp", "mac", get_mac_address().c_str()); -#ifdef ARDUINO_ARCH_ESP8266 - MDNS.addServiceTxt("esphomelib", "tcp", "platform", "ESP8266"); -#endif -#ifdef ARDUINO_ARCH_ESP32 - MDNS.addServiceTxt("esphomelib", "tcp", "platform", "ESP32"); -#endif - MDNS.addServiceTxt("esphomelib", "tcp", "board", ESPHOME_BOARD); -#ifdef ESPHOME_PROJECT_NAME - MDNS.addServiceTxt("esphomelib", "tcp", "project_name", ESPHOME_PROJECT_NAME); - MDNS.addServiceTxt("esphomelib", "tcp", "project_version", ESPHOME_PROJECT_VERSION); -#endif - } else { -#endif - // Publish "http" service if not using native API nor the webserver component - // This is just to have *some* mDNS service so that .local resolution works - MDNS.addService("http", "tcp", WEBSERVER_PORT); - MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); -#ifdef USE_API - } -#endif -#ifdef USE_PROMETHEUS - MDNS.addService("prometheus-http", "tcp", WEBSERVER_PORT); -#endif - } -#endif - - void network_tick_mdns() { -#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) - if (mdns_setup) - MDNS.update(); -#endif - } - - std::string network_get_address() { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) - return ethernet::global_eth_component->get_use_address(); -#endif -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->get_use_address(); -#endif - return ""; - } - } // namespace esphome diff --git a/esphome/core/util.h b/esphome/core/util.h index 2d58eff893..1ca0173eab 100644 --- a/esphome/core/util.h +++ b/esphome/core/util.h @@ -1,15 +1,8 @@ #pragma once #include -#include "IPAddress.h" - namespace esphome { -/// Return whether the node is connected to the network (through wifi, eth, ...) -bool network_is_connected(); -/// Get the active network hostname -std::string network_get_address(); - /// Return whether the node has at least one client connected to the native API bool api_is_connected(); @@ -19,14 +12,4 @@ bool mqtt_is_connected(); /// Return whether the node has any form of "remote" connection via the API or to an MQTT broker bool remote_is_connected(); -/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode) -#ifdef ARDUINO_ARCH_ESP8266 -void network_setup_mdns(IPAddress address, int interface); -#endif -#ifdef ARDUINO_ARCH_ESP32 -void network_setup_mdns(); -#endif - -void network_tick_mdns(); - } // namespace esphome diff --git a/esphome/core/version.h b/esphome/core/version.h index 0942c3e52f..7336ebf7f8 100644 --- a/esphome/core/version.h +++ b/esphome/core/version.h @@ -1,3 +1,12 @@ #pragma once -// This file is auto-generated! Do not edit! + +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the version for the current build. This is kept in its +// own file so that not all files have to be recompiled for each new release. +// +// This file is only used by static analyzers and IDEs. + +#include "esphome/core/macros.h" + #define ESPHOME_VERSION "dev" +#define ESPHOME_VERSION_CODE VERSION_CODE(2099, 12, 0) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 802e9a9d38..937b6cceb4 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -182,7 +182,7 @@ class ArrayInitializer(Expression): cpp += f" {arg},\n" cpp += "}" else: - cpp = "{" + ", ".join(str(arg) for arg in self.args) + "}" + cpp = f"{{{', '.join(str(arg) for arg in self.args)}}}" return cpp @@ -310,6 +310,31 @@ class FloatLiteral(Literal): return f"{self.f}f" +class BinOpExpression(Expression): + __slots__ = ("op", "lhs", "rhs") + + def __init__(self, lhs: SafeExpType, op: str, rhs: SafeExpType): + self.lhs = safe_exp(lhs) + self.op = op + self.rhs = safe_exp(rhs) + + def __str__(self): + # Surround with parentheses to ensure generated code has same + # order as python one + return f"({self.lhs} {self.op} {self.rhs})" + + +class UnaryOpExpression(Expression): + __slots__ = ("op", "exp") + + def __init__(self, op: str, exp: SafeExpType): + self.op = op + self.exp = safe_exp(exp) + + def __str__(self): + return f"({self.op}{self.exp})" + + def safe_exp(obj: SafeExpType) -> Expression: """Try to convert obj to an expression by automatically converting native python types to expressions/literals. @@ -348,13 +373,11 @@ def safe_exp(obj: SafeExpType) -> Expression: return float_ if isinstance(obj, ID): raise ValueError( - "Object {} is an ID. Did you forget to register the variable?" - "".format(obj) + f"Object {obj} is an ID. Did you forget to register the variable?" ) if inspect.isgenerator(obj): raise ValueError( - "Object {} is a coroutine. Did you forget to await the expression with " - "'await'?".format(obj) + f"Object {obj} is a coroutine. Did you forget to await the expression with 'await'?" ) raise ValueError("Object is not an expression", obj) @@ -543,13 +566,13 @@ def add_global(expression: Union[SafeExpType, Statement]): CORE.add_global(expression) -def add_library(name: str, version: Optional[str]): +def add_library(name: str, version: Optional[str], repository: Optional[str] = None): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') :param version: The version of the library, may be None. """ - CORE.add_library(Library(name, version)) + CORE.add_library(Library(name, version, repository)) def add_build_flag(build_flag: str): @@ -568,6 +591,10 @@ def add_define(name: str, value: SafeExpType = None): CORE.add_define(Define(name, safe_exp(value))) +def add_platformio_option(key: str, value: Union[str, List[str]]): + CORE.add_platformio_option(key, value) + + async def get_variable(id_: ID) -> "MockObj": """ Wait for the given ID to be defined in the code generation and @@ -611,7 +638,7 @@ async def process_lambda( :param return_type: The return type of the lambda. :return: The generated lambda expression. """ - from esphome.components.globals import GlobalsComponent + from esphome.components.globals import GlobalsComponent, RestoringGlobalsComponent if value is None: return @@ -621,7 +648,10 @@ async def process_lambda( if ( full_id is not None and isinstance(full_id.type, MockObjClass) - and full_id.type.inherits_from(GlobalsComponent) + and ( + full_id.type.inherits_from(GlobalsComponent) + or full_id.type.inherits_from(RestoringGlobalsComponent) + ) ): parts[i * 3 + 1] = var.value() continue @@ -703,7 +733,7 @@ class MockObj(Expression): return str(self.base) def __repr__(self): - return "MockObj<{}>".format(str(self.base)) + return f"MockObj<{str(self.base)}>" @property def _(self) -> "MockObj": @@ -714,6 +744,7 @@ class MockObj(Expression): return MockObj(f"new {self.base}", "->") def template(self, *args: SafeExpType) -> "MockObj": + """Apply template parameters to this object.""" if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: @@ -734,6 +765,10 @@ class MockObj(Expression): return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) def operator(self, name: str) -> "MockObj": + """Various other operations. + + Named operator because it's a C++ keyword and can't occur in valid code. + """ if name == "ref": return MockObj(f"{self.base} &", "") if name == "ptr": @@ -754,6 +789,162 @@ class MockObj(Expression): next_op = "->" return MockObj(f"{self.base}[{item}]", next_op) + def __lt__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "<", other) + return MockObj(op) + + def __le__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "<=", other) + return MockObj(op) + + def __eq__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "==", other) + return MockObj(op) + + def __ne__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "!=", other) + return MockObj(op) + + def __gt__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, ">", other) + return MockObj(op) + + def __ge__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, ">=", other) + return MockObj(op) + + def __add__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "+", other) + return MockObj(op) + + def __sub__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "-", other) + return MockObj(op) + + def __mul__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "*", other) + return MockObj(op) + + def __truediv__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "/", other) + return MockObj(op) + + def __mod__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "%", other) + return MockObj(op) + + def __lshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "<<", other) + return MockObj(op) + + def __rshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, ">>", other) + return MockObj(op) + + def __and__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "&", other) + return MockObj(op) + + def __xor__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "^", other) + return MockObj(op) + + def __or__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "|", other) + return MockObj(op) + + def __radd__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "+", self) + return MockObj(op) + + def __rsub__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "-", self) + return MockObj(op) + + def __rmul__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "*", self) + return MockObj(op) + + def __rtruediv__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "/", self) + return MockObj(op) + + def __rmod__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "%", self) + return MockObj(op) + + def __rlshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "<<", self) + return MockObj(op) + + def __rrshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, ">>", self) + return MockObj(op) + + def __rand__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "&", self) + return MockObj(op) + + def __rxor__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "^", self) + return MockObj(op) + + def __ror__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(other, "|", self) + return MockObj(op) + + def __iadd__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "+=", other) + return MockObj(op) + + def __isub__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "-=", other) + return MockObj(op) + + def __imul__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "*=", other) + return MockObj(op) + + def __itruediv__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "/=", other) + return MockObj(op) + + def __imod__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "%=", other) + return MockObj(op) + + def __ilshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "<<=", other) + return MockObj(op) + + def __irshift__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, ">>=", other) + return MockObj(op) + + def __iand__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "&=", other) + return MockObj(op) + + def __ixor__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "^=", other) + return MockObj(op) + + def __ior__(self, other: SafeExpType) -> "MockObj": + op = BinOpExpression(self, "|=", other) + return MockObj(op) + + def __neg__(self) -> "MockObj": + op = UnaryOpExpression("-", self) + return MockObj(op) + + def __pos__(self) -> "MockObj": + op = UnaryOpExpression("+", self) + return MockObj(op) + + def __invert__(self) -> "MockObj": + op = UnaryOpExpression("~", self) + return MockObj(op) + class MockObjEnum(MockObj): def __init__(self, *args, **kwargs): @@ -761,7 +952,7 @@ class MockObjEnum(MockObj): self._is_class = kwargs.pop("is_class") base = kwargs.pop("base") if self._is_class: - base = base + "::" + self._enum + base = f"{base}::{self._enum}" kwargs["op"] = "::" kwargs["base"] = base MockObj.__init__(self, *args, **kwargs) @@ -788,10 +979,10 @@ class MockObjClass(MockObj): self._parents += paren._parents def inherits_from(self, other: "MockObjClass") -> bool: - if self == other: + if str(self) == str(other): return True for parent in self._parents: - if parent == other: + if str(parent) == str(other): return True return False diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 1c52f38e50..5b081698ad 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,36 +1,39 @@ +import logging + from esphome.const import ( - CONF_INVERTED, - CONF_MODE, - CONF_NUMBER, + CONF_DISABLED_BY_DEFAULT, + CONF_ICON, + CONF_INTERNAL, + CONF_NAME, CONF_SETUP_PRIORITY, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, ) # pylint: disable=unused-import -from esphome.core import coroutine, ID, CORE, ConfigType -from esphome.cpp_generator import RawExpression, add, get_variable -from esphome.cpp_types import App, GPIOPin +from esphome.core import coroutine, ID, CORE +from esphome.types import ConfigType +from esphome.cpp_generator import add, get_variable +from esphome.cpp_types import App from esphome.util import Registry, RegistryEntry +_LOGGER = logging.getLogger(__name__) + + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. This is a coroutine, you must await it with a 'await' expression! """ if conf is None: - return + return None from esphome import pins for key, (func, _) in pins.PIN_SCHEMA_REGISTRY.items(): if key in conf: return await coroutine(func)(conf) - - number = conf[CONF_NUMBER] - mode = conf[CONF_MODE] - inverted = conf.get(CONF_INVERTED) - return GPIOPin.new(number, RawExpression(mode), inverted) + return await coroutine(pins.PIN_SCHEMA_REGISTRY[CORE.target_platform][0])(conf) async def register_component(var, config): @@ -41,18 +44,44 @@ async def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ + import inspect + id_ = str(var.base) if id_ not in CORE.component_ids: raise ValueError( - "Component ID {} was not declared to inherit from Component, " - "or was registered twice. Please create a bug report with your " - "configuration.".format(id_) + f"Component ID {id_} was not declared to inherit from Component, or was registered twice. Please create a bug report with your configuration." ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + + # Set component source by inspecting the stack and getting the callee module + # https://stackoverflow.com/a/1095621 + name = None + try: + for frm in inspect.stack()[1:]: + mod = inspect.getmodule(frm[0]) + if mod is None: + continue + name = mod.__name__ + if name.startswith("esphome.components."): + name = name[len("esphome.components.") :] + break + if name == "esphome.automation": + name = "automation" + # continue looking further up in stack in case we find a better one + if name == "esphome.coroutine": + # Only works for async-await coroutine syntax + break + except (KeyError, AttributeError, IndexError) as e: + _LOGGER.warning( + "Error while finding name of component, please report this", exc_info=e + ) + if name is not None: + add(var.set_component_source(name)) + add(App.register_component(var)) return var @@ -65,6 +94,16 @@ async def register_parented(var, value): add(var.set_parent(paren)) +async def setup_entity(var, config): + """Set up generic properties of an Entity""" + add(var.set_name(config[CONF_NAME])) + add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + if CONF_INTERNAL in config: + add(var.set_internal(config[CONF_INTERNAL])) + if CONF_ICON in config: + add(var.set_icon(config[CONF_ICON])) + + def extract_registry_entry_config(registry, full_config): # type: (Registry, ConfigType) -> RegistryEntry key, config = next((k, v) for k, v in full_config.items() if k in registry) diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 3036249a03..888c319024 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -13,12 +13,13 @@ std_vector = std_ns.class_("vector") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") +uint64 = global_ns.namespace("uint64_t") int32 = global_ns.namespace("int32_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; App = esphome_ns.App -Nameable = esphome_ns.class_("Nameable") +EntityBase = esphome_ns.class_("EntityBase") Component = esphome_ns.class_("Component") ComponentPtr = Component.operator("ptr") PollingComponent = esphome_ns.class_("PollingComponent", Component) @@ -29,5 +30,7 @@ JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectRef = JsonObject.operator("ref") JsonObjectConstRef = JsonObjectRef.operator("const") Controller = esphome_ns.class_("Controller") - GPIOPin = esphome_ns.class_("GPIOPin") +InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin) +gpio_ns = esphome_ns.namespace("gpio") +gpio_Flags = gpio_ns.enum("Flags", is_class=True) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 90061a3d4e..eb698a7de1 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -41,7 +41,7 @@ from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa -from esphome.zeroconf import DashboardStatus, Zeroconf +from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf _LOGGER = logging.getLogger(__name__) @@ -98,7 +98,7 @@ class DashboardSettings: return os.path.join(self.config_dir, *args) def list_yaml_files(self): - return util.list_yaml_files(self.config_dir) + return util.list_yaml_files([self.config_dir]) settings = DashboardSettings() @@ -154,9 +154,6 @@ def is_authenticated(request_handler): def bind_config(func): def decorator(self, *args, **kwargs): configuration = self.get_argument("configuration") - if not is_allowed(configuration): - self.set_status(500) - return None kwargs = kwargs.copy() kwargs["configuration"] = configuration return func(self, *args, **kwargs) @@ -329,7 +326,7 @@ class EsphomeVscodeHandler(EsphomeCommandWebSocket): class EsphomeAceEditorHandler(EsphomeCommandWebSocket): def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", settings.config_dir, "--ace"] + return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir] class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): @@ -363,17 +360,40 @@ class WizardRequestHandler(BaseHandler): from esphome import wizard kwargs = { - k: "".join(x.decode() for x in v) - for k, v in self.request.arguments.items() + k: v + for k, v in json.loads(self.request.body.decode()).items() if k in ("name", "platform", "board", "ssid", "psk", "password") } kwargs["ota_password"] = secrets.token_hex(16) - destination = settings.rel_path(kwargs["name"] + ".yaml") + destination = settings.rel_path(f"{kwargs['name']}.yaml") wizard.wizard_write(path=destination, **kwargs) self.set_status(200) 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"] + import_config( + settings.rel_path(f"{name}.yaml"), + name, + args["project_name"], + args["package_import_url"], + ) + except FileExistsError: + self.set_status(500) + self.write("File already exists") + return + + self.set_status(200) + self.finish() + + class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config @@ -431,7 +451,7 @@ class DashboardEntry: @property def name(self): if self.storage is None: - return self.filename[: -len(".yaml")] + return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name @property @@ -441,16 +461,10 @@ class DashboardEntry: return self.storage.comment @property - def esp_platform(self): + def target_platform(self): if self.storage is None: return None - return self.storage.esp_platform - - @property - def board(self): - if self.storage is None: - return None - return self.storage.board + return self.storage.target_platform @property def update_available(self): @@ -475,15 +489,51 @@ class DashboardEntry: 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, + "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, + "target_platform": entry.target_platform, + } + for entry in entries + ], + "importable": [ + { + "name": res.device_name, + "package_import_url": res.package_import_url, + "project_name": res.project_name, + "project_version": res.project_version, + } + 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)) - entries = _list_dashboard_entries() self.render( get_template_path("index"), - entries=entries, begin=begin, **template_args(), login_enabled=settings.using_auth, @@ -501,31 +551,38 @@ def _ping_func(filename, address): class MDNSStatusThread(threading.Thread): def run(self): - zc = Zeroconf() + 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() stat.request_query( - {entry.filename: entry.name + ".local." for entry in entries} + {entry.filename: f"{entry.name}.local." for entry in entries} ) + 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.is_set(): + while not STOP_EVENT.wait(2): # Only do pings if somebody has the dashboard open def callback(ret): @@ -573,10 +630,6 @@ class PingRequestHandler(BaseHandler): self.write(json.dumps(PING_RESULT)) -def is_allowed(configuration): - return os.path.sep not in configuration - - class InfoRequestHandler(BaseHandler): @authenticated @bind_config @@ -600,7 +653,7 @@ class EditRequestHandler(BaseHandler): content = "" if os.path.isfile(filename): # pylint: disable=no-value-for-parameter - with open(filename, "r") as f: + with open(file=filename, mode="r", encoding="utf-8") as f: content = f.read() self.write(content) @@ -608,7 +661,7 @@ class EditRequestHandler(BaseHandler): @bind_config def post(self, configuration=None): # pylint: disable=no-value-for-parameter - with open(settings.rel_path(configuration), "wb") as f: + with open(file=settings.rel_path(configuration), mode="wb") as f: f.write(self.request.body) self.set_status(200) @@ -619,20 +672,18 @@ class DeleteRequestHandler(BaseHandler): def post(self, configuration=None): config_file = settings.rel_path(configuration) storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.set_status(500) - return - name = storage_json.name trash_path = trash_storage_path(settings.config_dir) mkdir_p(trash_path) shutil.move(config_file, os.path.join(trash_path, configuration)) - # Delete build folder (if exists) - 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)) + 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): @@ -645,6 +696,7 @@ class UndoDeleteRequestHandler(BaseHandler): PING_RESULT = {} # type: dict +IMPORT_RESULT = {} STOP_EVENT = threading.Event() PING_REQUEST = threading.Event() @@ -716,9 +768,6 @@ class LogoutHandler(BaseHandler): self.redirect("./login") -_STATIC_FILE_HASHES = {} - - def get_base_frontend_path(): if ENV_DEV not in os.environ: import esphome_dashboard @@ -741,19 +790,23 @@ def get_static_path(*args): return os.path.join(get_base_frontend_path(), "static", *args) +@functools.lru_cache(maxsize=None) 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": - return f"./static/{name}" + import esphome_dashboard - if name in _STATIC_FILE_HASHES: - hash_ = _STATIC_FILE_HASHES[name] - else: - path = get_static_path(name) - with open(path, "rb") as f_handle: - hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] - _STATIC_FILE_HASHES[name] = hash_ - return f"./static/{name}?hash={hash_}" + 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)): @@ -781,10 +834,9 @@ def make_app(debug=get_bool_env(ENV_DEV)): class StaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): - if debug: - self.set_header( - "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0" - ) + self.set_header( + "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0" + ) app_settings = { "debug": debug, @@ -795,34 +847,33 @@ def make_app(debug=get_bool_env(ENV_DEV)): rel = settings.relative_url app = tornado.web.Application( [ - (rel + "", MainRequestHandler), - (rel + "login", LoginHandler), - (rel + "logout", LogoutHandler), - (rel + "logs", EsphomeLogsHandler), - (rel + "upload", EsphomeUploadHandler), - (rel + "compile", EsphomeCompileHandler), - (rel + "validate", EsphomeValidateHandler), - (rel + "clean-mqtt", EsphomeCleanMqttHandler), - (rel + "clean", EsphomeCleanHandler), - (rel + "vscode", EsphomeVscodeHandler), - (rel + "ace", EsphomeAceEditorHandler), - (rel + "update-all", EsphomeUpdateAllHandler), - (rel + "info", InfoRequestHandler), - (rel + "edit", EditRequestHandler), - (rel + "download.bin", DownloadBinaryRequestHandler), - (rel + "serial-ports", SerialPortRequestHandler), - (rel + "ping", PingRequestHandler), - (rel + "delete", DeleteRequestHandler), - (rel + "undo-delete", UndoDeleteRequestHandler), - (rel + "wizard.html", WizardRequestHandler), - (rel + r"static/(.*)", StaticFileHandler, {"path": get_static_path()}), + (f"{rel}", MainRequestHandler), + (f"{rel}login", LoginHandler), + (f"{rel}logout", LogoutHandler), + (f"{rel}logs", EsphomeLogsHandler), + (f"{rel}upload", EsphomeUploadHandler), + (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), ], **app_settings, ) - if debug: - _STATIC_FILE_HASHES.clear() - return app diff --git a/esphome/espota2.py b/esphome/espota2.py index 351f6feda9..f8a2fab94c 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -52,9 +52,7 @@ class ProgressBar: return self.last_progress = new_progress block = int(round(bar_length * progress)) - text = "\rUploading: [{0}] {1}% {2}".format( - "=" * block + " " * (bar_length - block), new_progress, status - ) + text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" sys.stderr.write(text) sys.stderr.flush() @@ -154,7 +152,7 @@ def check_error(data, expect): if not isinstance(expect, (list, tuple)): expect = [expect] if dat not in expect: - raise OTAError("Unexpected response from ESP: 0x{:02X}".format(data[0])) + raise OTAError(f"Unexpected response from ESP: 0x{data[0]:02X}") def send_check(sock, data, msg): diff --git a/esphome/final_validate.py b/esphome/final_validate.py new file mode 100644 index 0000000000..96dd2fd651 --- /dev/null +++ b/esphome/final_validate.py @@ -0,0 +1,58 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any +import contextvars + +from esphome.types import ConfigFragmentType, ID, ConfigPathType +import esphome.config_validation as cv + + +class FinalValidateConfig(ABC): + @property + @abstractmethod + def data(self) -> Dict[str, Any]: + """A dictionary that can be used by post validation functions to store + global data during the validation phase. Each component should store its + data under a unique key + """ + + @abstractmethod + def get_path_for_id(self, id: ID) -> ConfigPathType: + """Get the config path a given ID has been declared in. + + This is the location under the _validated_ config (for example, with cv.ensure_list applied) + Raises KeyError if the id was not declared in the configuration. + """ + + @abstractmethod + def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType: + """Get the config fragment for the given global path. + + Raises KeyError if a key in the path does not exist. + """ + + +FinalValidateConfig.register(dict) + +# Context variable tracking the full config for some final validation functions. +full_config: contextvars.ContextVar[FinalValidateConfig] = contextvars.ContextVar( + "full_config" +) + + +def id_declaration_match_schema(schema): + """A final-validation schema function that applies a schema to the outer config fragment of an + ID declaration. + + This validator must be applied to ID values. + """ + if not isinstance(schema, cv.Schema): + schema = cv.Schema(schema, extra=cv.ALLOW_EXTRA) + + def validator(value): + fconf = full_config.get() + path = fconf.get_path_for_id(value)[:-1] + declaration_config = fconf.get_config_for_path(path) + with cv.prepend_path([cv.ROOT_CONFIG_PATH] + path): + return schema(declaration_config) + + return validator diff --git a/esphome/git.py b/esphome/git.py new file mode 100644 index 0000000000..12c6b41648 --- /dev/null +++ b/esphome/git.py @@ -0,0 +1,74 @@ +from pathlib import Path +import subprocess +import hashlib +import logging + +from datetime import datetime + +from esphome.core import CORE, TimePeriodSeconds +import esphome.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + + +def run_git_command(cmd, cwd=None): + try: + ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + except FileNotFoundError as err: + raise cv.Invalid( + "git is not installed but required for external_components.\n" + "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" + ) from err + + if ret.returncode != 0 and ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise cv.Invalid(lines[-1][len("fatal: ") :]) + raise cv.Invalid(err_str) + + +def _compute_destination_path(key: str, domain: str) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / domain + h = hashlib.new("sha256") + h.update(key.encode()) + return base_dir / h.hexdigest()[:8] + + +def clone_or_update( + *, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str +) -> Path: + key = f"{url}@{ref}" + repo_dir = _compute_destination_path(key, domain) + 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: + cmd += ["--branch", ref] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) + if age.total_seconds() > refresh.total_seconds: + _LOGGER.info("Updating %s", key) + _LOGGER.debug("Location: %s", repo_dir) + # Stash local changes (if any) + run_git_command( + ["git", "stash", "push", "--include-untracked"], str(repo_dir) + ) + # Fetch remote ref + cmd = ["git", "fetch", "--", "origin"] + if ref is not None: + cmd.append(ref) + run_git_command(cmd, str(repo_dir)) + # 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)) + + return repo_dir diff --git a/esphome/helpers.py b/esphome/helpers.py index ad7b8272b2..1193d61eaa 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -54,7 +54,7 @@ def cpp_string_escape(string, encoding="utf-8"): result += f"\\{character:03o}" else: result += chr(character) - return '"' + result + '"' + return f'"{result}"' def run_system_command(*args): @@ -97,17 +97,17 @@ def is_ip_address(host): def _resolve_with_zeroconf(host): from esphome.core import EsphomeError - from esphome.zeroconf import Zeroconf + from esphome.zeroconf import EsphomeZeroconf try: - zc = Zeroconf() + zc = EsphomeZeroconf() except Exception as err: raise EsphomeError( "Cannot start mDNS sockets, is this a docker container without " "host network mode?" ) from err try: - info = zc.resolve_host(host + ".") + info = zc.resolve_host(f"{host}.") except Exception as err: raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err finally: @@ -136,9 +136,7 @@ def resolve_ip_address(host): return socket.gethostbyname(host) except OSError as err: errs.append(str(err)) - raise EsphomeError( - "Error resolving IP address: {}" "".format(", ".join(errs)) - ) from err + raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err def get_bool_env(var, default=False): @@ -211,15 +209,21 @@ def write_file(path: Union[Path, str], text: str): raise EsphomeError(f"Could not write file at {path}") from err -def write_file_if_changed(path: Union[Path, str], text: str): +def write_file_if_changed(path: Union[Path, str], text: str) -> bool: + """Write text to the given path, but not if the contents match already. + + Returns true if the file was changed. + """ if not isinstance(path, Path): path = Path(path) src_content = None if path.is_file(): src_content = read_file(path) - if src_content != text: - write_file(path, text) + if src_content == text: + return False + write_file(path, text) + return True def copy_file_if_changed(src: os.PathLike, dst: os.PathLike) -> None: @@ -276,11 +280,11 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: # A dict of types that need to be converted to heaptypes before a class can be added # to the object _TYPE_OVERLOADS = { - int: type("EInt", (int,), dict()), - float: type("EFloat", (float,), dict()), - str: type("EStr", (str,), dict()), - dict: type("EDict", (str,), dict()), - list: type("EList", (list,), dict()), + int: type("EInt", (int,), {}), + float: type("EFloat", (float,), {}), + str: type("EStr", (str,), {}), + dict: type("EDict", (str,), {}), + list: type("EList", (list,), {}), } # cache created classes here diff --git a/esphome/legacy.py b/esphome/legacy.py deleted file mode 100644 index 6b3b1d6c99..0000000000 --- a/esphome/legacy.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - - -def main(): - print("The esphomeyaml command has been renamed to esphome.") - print("") - print("$ esphome {}".format(" ".join(sys.argv[1:]))) - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/esphome/loader.py b/esphome/loader.py index d9d407d787..05d2e5a213 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,6 +1,5 @@ import logging -import typing -from typing import Callable, List, Optional, Dict, Any, ContextManager +from typing import Callable, List, Optional, Any, ContextManager from types import ModuleType import importlib import importlib.util @@ -8,28 +7,23 @@ import importlib.resources import importlib.abc import sys from pathlib import Path +from dataclasses import dataclass -from esphome.const import ESP_PLATFORMS, SOURCE_FILE_EXTENSIONS +from esphome.const import SOURCE_FILE_EXTENSIONS import esphome.core.config from esphome.core import CORE +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) -class SourceFile: - def __init__( - self, - package: importlib.resources.Package, - resource: importlib.resources.Resource, - ) -> None: - self._package = package - self._resource = resource - - def open_binary(self) -> typing.BinaryIO: - return importlib.resources.open_binary(self._package, self._resource) +@dataclass(frozen=True, order=True) +class FileResource: + package: str + resource: str def path(self) -> ContextManager[Path]: - return importlib.resources.path(self._package, self._resource) + return importlib.resources.path(self.package, self.resource) class ComponentManifest: @@ -38,6 +32,13 @@ class ComponentManifest: @property def package(self) -> str: + """Return the package name the module is contained in. + + Examples: + - esphome/components/gpio/__init__.py -> esphome.components.gpio + - esphome/components/gpio/switch/__init__.py -> esphome.components.gpio.switch + - esphome/components/a4988/stepper.py -> esphome.components.a4988 + """ return self.module.__package__ @property @@ -60,10 +61,6 @@ class ComponentManifest: def to_code(self) -> Optional[Callable[[Any], None]]: return getattr(self.module, "to_code", None) - @property - def esp_platforms(self) -> List[str]: - return getattr(self.module, "ESP_PLATFORMS", ESP_PLATFORMS) - @property def dependencies(self) -> List[str]: return getattr(self.module, "DEPENDENCIES", []) @@ -81,21 +78,29 @@ class ComponentManifest: return getattr(self.module, "CODEOWNERS", []) @property - def validate(self): - return getattr(self.module, "validate", None) + def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]: + """Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called + after the main validation. In that function checks across components can be made. + + Note that the function can't mutate the configuration - no changes are saved + """ + return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) @property - def source_files(self) -> Dict[Path, SourceFile]: - ret = {} + def resources(self) -> List[FileResource]: + """Return a list of all file resources defined in the package of this component. + + This will return all cpp source files that are located in the same folder as the + loaded .py file (does not look through subdirectories) + """ + ret = [] for resource in importlib.resources.contents(self.package): if Path(resource).suffix not in SOURCE_FILE_EXTENSIONS: continue if not importlib.resources.is_resource(self.package, resource): # Not a resource = this is a directory (yeah this is confusing) continue - # Always use / for C++ include names - target_path = Path(*self.package.split(".")) / resource - ret[target_path] = SourceFile(self.package, resource) + ret.append(FileResource(self.package, resource)) return ret diff --git a/esphome/log.py b/esphome/log.py index fa79efa833..abefcf6308 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -50,7 +50,7 @@ def color(col: str, msg: str, reset: bool = True) -> bool: class ESPHomeLogFormatter(logging.Formatter): def __init__(self): - super().__init__(fmt="%(levelname)s %(message)s", datefmt="%H:%M:%S", style="%") + super().__init__(fmt="%(asctime)s %(levelname)s %(message)s", style="%") def format(self, record): formatted = super().format(record) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 9be87b5c5d..07602e8ced 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -104,9 +104,9 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): if CONF_LOG_TOPIC in conf: topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: - topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + "/debug" + topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug" else: - topic = config[CONF_ESPHOME][CONF_NAME] + "/debug" + topic = f"{config[CONF_ESPHOME][CONF_NAME]}/debug" else: _LOGGER.error("MQTT isn't setup, can't start MQTT logs") return 1 @@ -158,9 +158,8 @@ def get_fingerprint(config): sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print("SHA1 Fingerprint: " + color(Fore.CYAN, sha1)) + safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}") safe_print( - "Copy the string above into mqtt.ssl_fingerprints section of {}" - "".format(CORE.config_path) + f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" ) return 0 diff --git a/esphome/pins.py b/esphome/pins.py index fef77f3946..2b3adce86d 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,1168 +1,143 @@ -import logging +import operator +from functools import reduce -import esphome.config_validation as cv -from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER -from esphome.core import CORE +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) from esphome.util import SimpleRegistry - -_LOGGER = logging.getLogger(__name__) - -ESP8266_BASE_PINS = { - "A0": 17, - "SS": 15, - "MOSI": 13, - "MISO": 12, - "SCK": 14, - "SDA": 4, - "SCL": 5, - "RX": 3, - "TX": 1, -} - -ESP8266_BOARD_PINS = { - "d1": { - "D0": 3, - "D1": 1, - "D2": 16, - "D3": 5, - "D4": 4, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 0, - "D9": 2, - "D10": 15, - "D11": 13, - "D12": 14, - "D13": 14, - "D14": 4, - "D15": 5, - "LED": 2, - }, - "d1_mini": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "LED": 2, - }, - "d1_mini_lite": "d1_mini", - "d1_mini_pro": "d1_mini", - "esp01": {}, - "esp01_1m": {}, - "esp07": {}, - "esp12e": {}, - "esp210": {}, - "esp8285": {}, - "esp_wroom_02": {}, - "espduino": {"LED": 16}, - "espectro": {"LED": 15, "BUTTON": 2}, - "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, - "espinotee": {"LED": 16}, - "espresso_lite_v1": {"LED": 16}, - "espresso_lite_v2": {"LED": 2}, - "gen4iod": {}, - "heltec_wifi_kit_8": "d1_mini", - "huzzah": { - "LED": 0, - "LED_RED": 0, - "LED_BLUE": 2, - "D4": 4, - "D5": 5, - "D12": 12, - "D13": 13, - "D14": 14, - "D15": 15, - "D16": 16, - }, - "inventone": {}, - "modwifi": {}, - "nodemcu": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - "LED": 16, - }, - "nodemcuv2": "nodemcu", - "oak": { - "P0": 2, - "P1": 5, - "P2": 0, - "P3": 3, - "P4": 1, - "P5": 4, - "P6": 15, - "P7": 13, - "P8": 12, - "P9": 14, - "P10": 16, - "P11": 17, - "LED": 5, - }, - "phoenix_v1": {"LED": 16}, - "phoenix_v2": {"LED": 2}, - "sparkfunBlynk": "thing", - "thing": {"LED": 5, "SDA": 2, "SCL": 14}, - "thingdev": "thing", - "wifi_slot": {"LED": 2}, - "wifiduino": { - "D0": 3, - "D1": 1, - "D2": 2, - "D3": 0, - "D4": 4, - "D5": 5, - "D6": 16, - "D7": 14, - "D8": 12, - "D9": 13, - "D10": 15, - "D11": 13, - "D12": 12, - "D13": 14, - }, - "wifinfo": { - "LED": 12, - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - }, - "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, - "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, - "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, -} - -FLASH_SIZE_1_MB = 2 ** 20 -FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 -FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB -FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB -FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB - -ESP8266_FLASH_SIZES = { - "d1": FLASH_SIZE_4_MB, - "d1_mini": FLASH_SIZE_4_MB, - "d1_mini_lite": FLASH_SIZE_1_MB, - "d1_mini_pro": FLASH_SIZE_16_MB, - "esp01": FLASH_SIZE_512_KB, - "esp01_1m": FLASH_SIZE_1_MB, - "esp07": FLASH_SIZE_4_MB, - "esp12e": FLASH_SIZE_4_MB, - "esp210": FLASH_SIZE_4_MB, - "esp8285": FLASH_SIZE_1_MB, - "esp_wroom_02": FLASH_SIZE_2_MB, - "espduino": FLASH_SIZE_4_MB, - "espectro": FLASH_SIZE_4_MB, - "espino": FLASH_SIZE_4_MB, - "espinotee": FLASH_SIZE_4_MB, - "espresso_lite_v1": FLASH_SIZE_4_MB, - "espresso_lite_v2": FLASH_SIZE_4_MB, - "gen4iod": FLASH_SIZE_512_KB, - "heltec_wifi_kit_8": FLASH_SIZE_4_MB, - "huzzah": FLASH_SIZE_4_MB, - "inventone": FLASH_SIZE_4_MB, - "modwifi": FLASH_SIZE_2_MB, - "nodemcu": FLASH_SIZE_4_MB, - "nodemcuv2": FLASH_SIZE_4_MB, - "oak": FLASH_SIZE_4_MB, - "phoenix_v1": FLASH_SIZE_4_MB, - "phoenix_v2": FLASH_SIZE_4_MB, - "sparkfunBlynk": FLASH_SIZE_4_MB, - "thing": FLASH_SIZE_512_KB, - "thingdev": FLASH_SIZE_512_KB, - "wifi_slot": FLASH_SIZE_1_MB, - "wifiduino": FLASH_SIZE_4_MB, - "wifinfo": FLASH_SIZE_1_MB, - "wio_link": FLASH_SIZE_4_MB, - "wio_node": FLASH_SIZE_4_MB, - "xinabox_cw01": FLASH_SIZE_4_MB, -} - -ESP8266_LD_SCRIPTS = { - FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), - FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), - FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), - FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), - FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), -} - -ESP32_BASE_PINS = { - "TX": 1, - "RX": 3, - "SDA": 21, - "SCL": 22, - "SS": 5, - "MOSI": 23, - "MISO": 19, - "SCK": 18, - "A0": 36, - "A3": 39, - "A4": 32, - "A5": 33, - "A6": 34, - "A7": 35, - "A10": 4, - "A11": 0, - "A12": 2, - "A13": 15, - "A14": 13, - "A15": 12, - "A16": 14, - "A17": 27, - "A18": 25, - "A19": 26, - "T0": 4, - "T1": 0, - "T2": 2, - "T3": 15, - "T4": 13, - "T5": 12, - "T6": 14, - "T7": 27, - "T8": 33, - "T9": 32, - "DAC1": 25, - "DAC2": 26, - "SVP": 36, - "SVN": 39, -} - -ESP32_BOARD_PINS = { - "alksesp32": { - "A0": 32, - "A1": 33, - "A2": 25, - "A3": 26, - "A4": 27, - "A5": 14, - "A6": 12, - "A7": 15, - "D0": 40, - "D1": 41, - "D10": 19, - "D11": 21, - "D12": 22, - "D13": 23, - "D2": 15, - "D3": 2, - "D4": 0, - "D5": 4, - "D6": 16, - "D7": 17, - "D8": 5, - "D9": 18, - "DHT_PIN": 26, - "LED": 23, - "L_B": 5, - "L_G": 17, - "L_R": 22, - "L_RGB_B": 16, - "L_RGB_G": 21, - "L_RGB_R": 4, - "L_Y": 23, - "MISO": 22, - "MOSI": 21, - "PHOTO": 25, - "PIEZO1": 19, - "PIEZO2": 18, - "POT1": 32, - "POT2": 33, - "S1": 4, - "S2": 16, - "S3": 18, - "S4": 19, - "S5": 21, - "SCK": 23, - "SCL": 14, - "SDA": 27, - "SS": 19, - "SW1": 15, - "SW2": 2, - "SW3": 0, - }, - "bpi-bit": { - "BUTTON_A": 35, - "BUTTON_B": 27, - "BUZZER": 25, - "LIGHT_SENSOR1": 36, - "LIGHT_SENSOR2": 39, - "MPU9250_INT": 0, - "P0": 25, - "P1": 32, - "P10": 26, - "P11": 27, - "P12": 2, - "P13": 18, - "P14": 19, - "P15": 23, - "P16": 5, - "P19": 22, - "P2": 33, - "P20": 21, - "P3": 13, - "P4": 15, - "P5": 35, - "P6": 12, - "P7": 14, - "P8": 16, - "P9": 17, - "RGB_LED": 4, - "TEMPERATURE_SENSOR": 34, - }, - "d-duino-32": { - "D1": 5, - "D10": 1, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 4, - "SDA": 5, - "SS": 15, - }, - "esp-wrover-kit": {}, - "esp32-devkitlipo": {}, - "esp32-evb": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, - "esp32-poe-iso": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - }, - "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, - "esp32-pro": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp320": { - "LED": 5, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 14, - "SDA": 2, - "SS": 15, - }, - "esp32cam": {}, - "esp32dev": {}, - "esp32doit-devkit-v1": {"LED": 2}, - "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, - "esp32vn-iot-uno": {}, - "espea32": {"BUTTON": 0, "LED": 5}, - "espectro32": {"LED": 15, "SD_SS": 33}, - "espino32": {"BUTTON": 0, "LED": 16}, - "featheresp32": { - "A0": 26, - "A1": 25, - "A10": 27, - "A11": 12, - "A12": 13, - "A13": 35, - "A2": 34, - "A4": 36, - "A5": 4, - "A6": 14, - "A7": 32, - "A8": 15, - "A9": 33, - "Ax": 2, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 33, - "TX": 17, - }, - "firebeetle32": {"LED": 2}, - "fm-devkit": { - "D0": 34, - "D1": 35, - "D10": 0, - "D2": 32, - "D3": 33, - "D4": 27, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 23, - "I2S_DOUT": 22, - "I2S_LRCLK": 25, - "I2S_MCLK": 2, - "I2S_SCLK": 26, - "LED": 5, - "SCL": 17, - "SDA": 16, - "SW1": 4, - "SW2": 18, - "SW3": 19, - "SW4": 21, - }, - "frogboard": {}, - "heltec_wifi_kit_32": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 25, - "RST_OLED": 16, - "SCL_OLED": 15, - "SDA_OLED": 4, - "Vext": 21, - }, - "heltec_wifi_lora_32": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 33, - "DIO2": 32, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wifi_lora_32_V2": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wireless_stick": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "hornbill32dev": {"BUTTON": 0, "LED": 13}, - "hornbill32minima": {"SS": 2}, - "intorobot": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 4, - "MISO": 17, - "MOSI": 16, - "RGB_B_BUILTIN": 22, - "RGB_G_BUILTIN": 21, - "RGB_R_BUILTIN": 27, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "iotaap_magnolia": {}, - "iotbusio": {}, - "iotbusproteus": {}, - "lolin32": {"LED": 5}, - "lolin32-lite": {"LED": 22}, - "lolin_d32": {"LED": 5, "_VBAT": 35}, - "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, - "lopy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "lopy4": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 18, - }, - "m5stack-core-esp32": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stack-fire": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - }, - "m5stack-grey": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stick-c": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G10": 10, - "G26": 26, - "G32": 32, - "G33": 33, - "G36": 36, - "G37": 37, - "G39": 39, - "G9": 9, - "MISO": 36, - "MOSI": 15, - "SCK": 13, - "SCL": 33, - "SDA": 32, - }, - "magicbit": { - "BLUE_LED": 17, - "BUZZER": 25, - "GREEN_LED": 16, - "LDR": 36, - "LED": 16, - "LEFT_BUTTON": 35, - "MOTOR1A": 27, - "MOTOR1B": 18, - "MOTOR2A": 16, - "MOTOR2B": 17, - "POT": 39, - "RED_LED": 27, - "RIGHT_PUTTON": 34, - "YELLOW_LED": 18, - }, - "mhetesp32devkit": {"LED": 2}, - "mhetesp32minikit": {"LED": 2}, - "microduino-core-esp32": { - "A0": 12, - "A1": 13, - "A10": 25, - "A11": 26, - "A12": 27, - "A13": 14, - "A2": 15, - "A3": 4, - "A6": 38, - "A7": 37, - "A8": 32, - "A9": 33, - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 23, - "D12": 19, - "D13": 18, - "D14": 12, - "D15": 13, - "D16": 15, - "D17": 4, - "D18": 22, - "D19": 21, - "D2": 16, - "D20": 38, - "D21": 37, - "D3": 17, - "D4": 32, - "D5": 33, - "D6": 25, - "D7": 26, - "D8": 27, - "D9": 14, - "SCL": 21, - "SCL1": 13, - "SDA": 22, - "SDA1": 12, - }, - "nano32": {"BUTTON": 0, "LED": 16}, - "nina_w10": { - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 19, - "D12": 23, - "D13": 18, - "D14": 13, - "D15": 12, - "D16": 32, - "D17": 33, - "D18": 21, - "D19": 34, - "D2": 26, - "D20": 36, - "D21": 39, - "D3": 25, - "D4": 35, - "D5": 27, - "D6": 22, - "D7": 0, - "D8": 15, - "D9": 14, - "LED_BLUE": 21, - "LED_GREEN": 33, - "LED_RED": 23, - "SCL": 13, - "SDA": 12, - "SW1": 33, - "SW2": 27, - }, - "node32s": {}, - "nodemcu-32s": {"BUTTON": 0, "LED": 2}, - "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, - "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, - "oroca_edubot": { - "A0": 34, - "A1": 39, - "A2": 36, - "A3": 33, - "D0": 4, - "D1": 16, - "D2": 17, - "D3": 22, - "D4": 23, - "D5": 5, - "D6": 18, - "D7": 19, - "D8": 33, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 2, - "TX": 17, - "VBAT": 35, - }, - "pico32": {}, - "pocket_32": {"LED": 16}, - "pycom_gpy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "quantum": {}, - "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, - "tinypico": {}, - "ttgo-lora32-v1": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 2, - "MOSI": 27, - "SCK": 5, - "SS": 18, - }, - "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, - "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, - "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, - "ttgo-t7-v13-mini32": {"LED": 22}, - "ttgo-t7-v14-mini32": {"LED": 19}, - "turta_iot_node": {}, - "vintlabs-devkit-v1": { - "LED": 2, - "PWM0": 12, - "PWM1": 13, - "PWM2": 14, - "PWM3": 15, - "PWM4": 16, - "PWM5": 17, - "PWM6": 18, - "PWM7": 19, - }, - "wemos_d1_mini32": { - "D0": 26, - "D1": 22, - "D2": 21, - "D3": 17, - "D4": 16, - "D5": 18, - "D6": 19, - "D7": 23, - "D8": 5, - "LED": 2, - "RXD": 3, - "TXD": 1, - "_VBAT": 35, - }, - "wemosbat": {"LED": 16}, - "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, - "widora-air": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 25, - "MISO": 17, - "MOSI": 16, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "xinabox_cw02": {"LED": 27}, -} - - -def _lookup_pin(value): - if CORE.is_esp8266: - board_pins_dict = ESP8266_BOARD_PINS - base_pins = ESP8266_BASE_PINS - elif CORE.is_esp32: - board_pins_dict = ESP32_BOARD_PINS - base_pins = ESP32_BASE_PINS - else: - raise NotImplementedError - - board_pins = board_pins_dict.get(CORE.board, {}) - - # Resolved aliased board pins (shorthand when two boards have the same pin configuration) - while isinstance(board_pins, str): - board_pins = board_pins_dict[board_pins] - - if value in board_pins: - return board_pins[value] - if value in base_pins: - return base_pins[value] - raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {CORE.board}.") - - -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): - return value - try: - return int(value) - except ValueError: - pass - if value.startswith("GPIO"): - return cv.Coerce(int)(value[len("GPIO") :].strip()) - return _lookup_pin(value) - - -_ESP_SDIO_PINS = { - 6: "Flash Clock", - 7: "Flash Data 0", - 8: "Flash Data 1", - 11: "Flash Command", -} - - -def validate_gpio_pin(value): - value = _translate_pin(value) - if CORE.is_esp32: - if value < 0 or value > 39: - raise cv.Invalid(f"ESP32: Invalid pin number: {value}") - if value in _ESP_SDIO_PINS: - raise cv.Invalid( - "This pin cannot be used on ESP32s and is already used by " - "the flash interface (function: {})".format(_ESP_SDIO_PINS[value]) - ) - if 9 <= value <= 10: - _LOGGER.warning( - "ESP32: Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", - value, - ) - if value in (20, 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 - raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") - return value - if CORE.is_esp8266: - if value < 0 or value > 17: - raise cv.Invalid(f"ESP8266: Invalid pin number: {value}") - if value in _ESP_SDIO_PINS: - raise cv.Invalid( - "This pin cannot be used on ESP8266s and is already used by " - "the flash interface (function: {})".format(_ESP_SDIO_PINS[value]) - ) - if 9 <= value <= 10: - _LOGGER.warning( - "ESP8266: Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", - value, - ) - return value - raise NotImplementedError - - -def input_pin(value): - value = validate_gpio_pin(value) - if CORE.is_esp8266 and value == 17: - raise cv.Invalid("GPIO17 (TOUT) is an analog-only pin on the ESP8266.") - return value - - -def input_pullup_pin(value): - value = input_pin(value) - if CORE.is_esp32: - return output_pin(value) - if CORE.is_esp8266: - if value == 0: - raise cv.Invalid( - "GPIO Pin 0 does not support pullup pin mode. " - "Please choose another pin." - ) - return value - raise NotImplementedError - - -def output_pin(value): - value = validate_gpio_pin(value) - if CORE.is_esp32: - if 34 <= value <= 39: - raise cv.Invalid( - "ESP32: GPIO{} (34-39) can only be used as an " - "input pin.".format(value) - ) - return value - if CORE.is_esp8266: - if value == 17: - raise cv.Invalid("GPIO17 (TOUT) is an analog-only pin on the ESP8266.") - return value - raise NotImplementedError - - -def analog_pin(value): - value = validate_gpio_pin(value) - if CORE.is_esp32: - if 32 <= value <= 39: # ADC1 - return value - raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") - if CORE.is_esp8266: - if value == 17: # A0 - return value - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") - raise NotImplementedError - - -input_output_pin = cv.All(input_pin, output_pin) - -PIN_MODES_ESP8266 = [ - "INPUT", - "OUTPUT", - "INPUT_PULLUP", - "OUTPUT_OPEN_DRAIN", - "SPECIAL", - "FUNCTION_1", - "FUNCTION_2", - "FUNCTION_3", - "FUNCTION_4", - "FUNCTION_0", - "WAKEUP_PULLUP", - "WAKEUP_PULLDOWN", - "INPUT_PULLDOWN_16", -] -PIN_MODES_ESP32 = [ - "INPUT", - "OUTPUT", - "INPUT_PULLUP", - "OUTPUT_OPEN_DRAIN", - "SPECIAL", - "FUNCTION_1", - "FUNCTION_2", - "FUNCTION_3", - "FUNCTION_4", - "PULLUP", - "PULLDOWN", - "INPUT_PULLDOWN", - "OPEN_DRAIN", - "FUNCTION_5", - "FUNCTION_6", - "ANALOG", -] - - -def pin_mode(value): - if CORE.is_esp32: - return cv.one_of(*PIN_MODES_ESP32, upper=True)(value) - if CORE.is_esp8266: - return cv.one_of(*PIN_MODES_ESP8266, upper=True)(value) - raise NotImplementedError - - -GPIO_FULL_OUTPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_NUMBER): output_pin, - cv.Optional(CONF_MODE, default="OUTPUT"): pin_mode, - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) - -GPIO_FULL_INPUT_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_NUMBER): input_pin, - cv.Optional(CONF_MODE, default="INPUT"): pin_mode, - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) - -GPIO_FULL_INPUT_PULLUP_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_NUMBER): input_pin, - cv.Optional(CONF_MODE, default="INPUT_PULLUP"): pin_mode, - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } -) - -GPIO_FULL_ANALOG_PIN_SCHEMA = cv.Schema( - { - cv.Required(CONF_NUMBER): analog_pin, - cv.Optional(CONF_MODE, default="INPUT"): pin_mode, - } -) - - -def shorthand_output_pin(value): - value = output_pin(value) - return GPIO_FULL_OUTPUT_PIN_SCHEMA({CONF_NUMBER: value}) - - -def shorthand_input_pin(value): - value = input_pin(value) - return GPIO_FULL_INPUT_PIN_SCHEMA({CONF_NUMBER: value}) - - -def shorthand_input_pullup_pin(value): - value = input_pullup_pin(value) - return GPIO_FULL_INPUT_PIN_SCHEMA( - { - CONF_NUMBER: value, - CONF_MODE: "INPUT_PULLUP", - } - ) - - -def shorthand_analog_pin(value): - value = analog_pin(value) - return GPIO_FULL_INPUT_PIN_SCHEMA({CONF_NUMBER: value}) - - -def validate_has_interrupt(value): - if CORE.is_esp8266: - if value[CONF_NUMBER] >= 16: - raise cv.Invalid( - "Pins GPIO16 and GPIO17 do not support interrupts and cannot be used " - "here, got {}".format(value[CONF_NUMBER]) - ) - return value +from esphome.core import CORE PIN_SCHEMA_REGISTRY = SimpleRegistry() -def internal_gpio_output_pin_schema(value): - if isinstance(value, dict): - return GPIO_FULL_OUTPUT_PIN_SCHEMA(value) - return shorthand_output_pin(value) +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] + if not isinstance(mode, str): + return value + # mode is a string, try parsing it like arduino pin modes + PIN_MODES = { + "INPUT": { + CONF_INPUT: True, + }, + "OUTPUT": { + CONF_OUTPUT: True, + }, + "INPUT_PULLUP": { + CONF_INPUT: True, + CONF_PULLUP: True, + }, + "OUTPUT_OPEN_DRAIN": { + CONF_OUTPUT: True, + CONF_OPEN_DRAIN: True, + }, + "INPUT_PULLDOWN_16": { + CONF_INPUT: True, + CONF_PULLDOWN: True, + }, + "INPUT_PULLDOWN": { + CONF_INPUT: True, + CONF_PULLDOWN: True, + }, + } + if mode.upper() not in PIN_MODES: + raise cv.Invalid(f"Unknown pin mode {mode}", [CONF_MODE]) + return {**value, CONF_MODE: PIN_MODES[mode.upper()]} -def gpio_output_pin_schema(value): - if isinstance(value, dict): - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key in value: - return entry[1][0](value) - return internal_gpio_output_pin_schema(value) +def _schema_creator(default_mode, internal: bool = False): + def validator(value): + 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) + + return validator -def internal_gpio_input_pin_schema(value): - if isinstance(value, dict): - return GPIO_FULL_INPUT_PIN_SCHEMA(value) - return shorthand_input_pin(value) +def _internal_number_creator(mode): + def validator(value): + 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 validator -def internal_gpio_analog_pin_schema(value): - if isinstance(value, dict): - return GPIO_FULL_ANALOG_PIN_SCHEMA(value) - return shorthand_analog_pin(value) +def gpio_flags_expr(mode): + """Convert the given mode dict to a gpio Flags expression""" + import esphome.codegen as cg + + FLAGS_MAPPING = { + CONF_INPUT: cg.gpio_Flags.FLAG_INPUT, + CONF_OUTPUT: cg.gpio_Flags.FLAG_OUTPUT, + CONF_OPEN_DRAIN: cg.gpio_Flags.FLAG_OPEN_DRAIN, + CONF_PULLUP: cg.gpio_Flags.FLAG_PULLUP, + CONF_PULLDOWN: cg.gpio_Flags.FLAG_PULLDOWN, + } + active_flags = [v for k, v in FLAGS_MAPPING.items() if mode.get(k)] + if not active_flags: + return cg.gpio_Flags.FLAG_NONE + + return reduce(operator.or_, active_flags) -def gpio_input_pin_schema(value): - if isinstance(value, dict): - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key in value: - return entry[1][1](value) - return internal_gpio_input_pin_schema(value) - - -def internal_gpio_input_pullup_pin_schema(value): - if isinstance(value, dict): - return GPIO_FULL_INPUT_PULLUP_PIN_SCHEMA(value) - return shorthand_input_pullup_pin(value) - - -def gpio_input_pullup_pin_schema(value): - if isinstance(value, dict): - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key in value: - return entry[1][1](value) - return internal_gpio_input_pullup_pin_schema(value) +gpio_pin_schema = _schema_creator +internal_gpio_pin_number = _internal_number_creator +gpio_output_pin_schema = _schema_creator( + { + CONF_OUTPUT: True, + } +) +gpio_input_pin_schema = _schema_creator( + { + CONF_INPUT: True, + } +) +gpio_input_pullup_pin_schema = _schema_creator( + { + CONF_INPUT: True, + CONF_PULLUP: True, + } +) +internal_gpio_output_pin_schema = _schema_creator( + { + CONF_OUTPUT: True, + }, + internal=True, +) +internal_gpio_output_pin_number = _internal_number_creator({CONF_OUTPUT: True}) +internal_gpio_input_pin_schema = _schema_creator( + { + CONF_INPUT: True, + }, + internal=True, +) +internal_gpio_input_pin_number = _internal_number_creator({CONF_INPUT: True}) +internal_gpio_input_pullup_pin_schema = _schema_creator( + { + CONF_INPUT: True, + CONF_PULLUP: True, + }, + internal=True, +) +internal_gpio_input_pullup_pin_number = _internal_number_creator( + { + CONF_INPUT: True, + CONF_PULLUP: True, + } +) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 87ca12c9a8..054c0cb1b0 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,12 +1,15 @@ +from dataclasses import dataclass import json -from typing import Union +from typing import List, Union +from pathlib import Path import logging import os import re import subprocess -from esphome.core import CORE +from esphome.const import KEY_CORE +from esphome.core import CORE, EsphomeError from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) @@ -39,7 +42,7 @@ def patch_structhash(): command.clean_build_dir = patched_clean_build_dir -IGNORE_LIB_WARNINGS = r"(?:" + "|".join(["Hash", "Update"]) + r")" +IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" FILTER_PLATFORMIO_LINES = [ r"Verbose mode can be enabled via `-v, --verbose` option.*", r"CONFIGURATION: https://docs.platformio.org/.*", @@ -48,13 +51,9 @@ FILTER_PLATFORMIO_LINES = [ r"PACKAGES: .*", r"LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*", r"LDF Modes: Finder ~ chain, Compatibility ~ soft.*", - r"Looking for " + IGNORE_LIB_WARNINGS + r" library in registry", - r"Warning! Library `.*'" - + IGNORE_LIB_WARNINGS - + r".*` has not been found in PlatformIO Registry.", - r"You can ignore this message, if `.*" - + IGNORE_LIB_WARNINGS - + r".*` is a built-in library.*", + f"Looking for {IGNORE_LIB_WARNINGS} library in registry", + f"Warning! Library `.*'{IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.", + f"You can ignore this message, if `.*{IGNORE_LIB_WARNINGS}.*` is a built-in library.*", r"Scanning dependencies...", r"Found \d+ compatible libraries", r"Memory Usage -> http://bit.ly/pio-memory-usage", @@ -71,8 +70,8 @@ FILTER_PLATFORMIO_LINES = [ def run_platformio_cli(*args, **kwargs) -> Union[str, int]: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path()) - os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath( - CORE.relative_piolibdeps_path() + os.environ.setdefault( + "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) ) cmd = ["platformio"] + list(args) @@ -100,36 +99,56 @@ def run_compile(config, verbose): return run_platformio_cli_run(config, verbose) -def run_upload(config, verbose, port): - return run_platformio_cli_run( - config, verbose, "-t", "upload", "--upload-port", port - ) - - -def run_idedata(config): +def _run_idedata(config): args = ["-t", "idedata"] stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) match = re.search(r'{\s*".*}', stdout) if match is None: - _LOGGER.debug("Could not match IDEData for %s", stdout) - return IDEData(None) + _LOGGER.error("Could not match idedata, please report this error") + _LOGGER.error("Stdout: %s", stdout) + raise EsphomeError + try: - return IDEData(json.loads(match.group())) + return json.loads(match.group()) except ValueError: - _LOGGER.debug("Could not load IDEData for %s", stdout, exc_info=1) - return IDEData(None) + _LOGGER.error("Could not parse idedata", exc_info=True) + _LOGGER.error("Stdout: %s", stdout) + raise -IDE_DATA = None +def _load_idedata(config): + platformio_ini = Path(CORE.relative_build_path("platformio.ini")) + temp_idedata = Path(CORE.relative_internal_path(CORE.name, "idedata.json")) + + changed = False + if not platformio_ini.is_file() or not temp_idedata.is_file(): + changed = True + elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + changed = True + + if not changed: + try: + return json.loads(temp_idedata.read_text(encoding="utf-8")) + except ValueError: + pass + + temp_idedata.parent.mkdir(exist_ok=True, parents=True) + + data = _run_idedata(config) + + temp_idedata.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return data -def get_idedata(config): - global IDE_DATA +KEY_IDEDATA = "idedata" - if IDE_DATA is None: - _LOGGER.info("Need to fetch platformio IDE-data, please stand by") - IDE_DATA = run_idedata(config) - return IDE_DATA + +def get_idedata(config) -> "IDEData": + if KEY_IDEDATA in CORE.data[KEY_CORE]: + return CORE.data[KEY_CORE][KEY_IDEDATA] + idedata = IDEData(_load_idedata(config)) + CORE.data[KEY_CORE][KEY_IDEDATA] = idedata + return idedata # ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder @@ -200,13 +219,15 @@ def _parse_register(config, regex, line): STACKTRACE_ESP8266_EXCEPTION_TYPE_RE = re.compile(r"[eE]xception \((\d+)\):") STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})") STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})") -STACKTRACE_ESP32_PC_RE = re.compile(r"PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*") STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_BAD_ALLOC_RE = re.compile( r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" ) STACKTRACE_ESP32_BACKTRACE_RE = re.compile( - r"Backtrace:(?:\s+0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" + r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" ) STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") @@ -228,6 +249,9 @@ def process_stacktrace(config, line, backtrace_state): # ESP32 PC/EXCVADDR _parse_register(config, STACKTRACE_ESP32_PC_RE, line) _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) + # ESP32-C3 PC/RA + _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) + _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) # bad alloc match = re.match(STACKTRACE_BAD_ALLOC_RE, line) @@ -260,37 +284,42 @@ def process_stacktrace(config, line, backtrace_state): return backtrace_state +@dataclass +class FlashImage: + path: str + offset: str + + class IDEData: def __init__(self, raw): - if not isinstance(raw, dict): - self.raw = {} - else: - self.raw = raw + self.raw = raw @property def firmware_elf_path(self): - return self.raw.get("prog_path") + return self.raw["prog_path"] @property - def flash_extra_images(self): + def firmware_bin_path(self) -> str: + return str(Path(self.firmware_elf_path).with_suffix(".bin")) + + @property + def extra_flash_images(self) -> List[FlashImage]: return [ - (x["path"], x["offset"]) for x in self.raw.get("flash_extra_images", []) + FlashImage(path=entry["path"], offset=entry["offset"]) + for entry in self.raw["extra"]["flash_images"] ] @property - def cc_path(self): + def cc_path(self) -> str: # For example /Users//.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc - return self.raw.get("cc_path") + return self.raw["cc_path"] @property - def addr2line_path(self): - cc_path = self.cc_path - if cc_path is None: - return None + def addr2line_path(self) -> str: # replace gcc at end with addr2line # Windows - if cc_path.endswith(".exe"): - return cc_path[:-7] + "addr2line.exe" + if self.cc_path.endswith(".exe"): + return f"{self.cc_path[:-7]}addr2line.exe" - return cc_path[:-3] + "addr2line" + return f"{self.cc_path[:-3]}addr2line" diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 6f81e0d96a..3262559116 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -4,20 +4,19 @@ from datetime import datetime import json import logging import os +from typing import Any, Optional, List from esphome import const from esphome.core import CORE from esphome.helpers import write_file_if_changed -# pylint: disable=unused-import, wrong-import-order -from esphome.core import CoreType -from typing import Any, Optional, List +from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) def storage_path(): # type: () -> str - return CORE.relative_config_path(".esphome", f"{CORE.config_filename}.json") + return CORE.relative_internal_path(f"{CORE.config_filename}.json") def ext_storage_path(base_path, config_filename): # type: (str, str) -> str @@ -41,10 +40,8 @@ class StorageJSON: comment, esphome_version, src_version, - arduino_version, address, - esp_platform, - board, + target_platform, build_path, firmware_bin_path, loaded_integrations, @@ -61,15 +58,10 @@ class StorageJSON: # 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 = src_version # type: int - # The version of the Arduino framework, the build files need to be cleared each time - # this changes - self.arduino_version = arduino_version # type: str # Address of the ESP, for example livingroom.local or a static IP self.address = address # type: str # The type of ESP in use, either ESP32 or ESP8266 - self.esp_platform = esp_platform # type: str - # The ESP board used, for example nodemcuv2 - self.board = board # type: str + self.target_platform = target_platform # type: str # The absolute path to the platformio project self.build_path = build_path # type: str # The absolute path to the firmware binary @@ -85,17 +77,15 @@ class StorageJSON: "comment": self.comment, "esphome_version": self.esphome_version, "src_version": self.src_version, - "arduino_version": self.arduino_version, "address": self.address, - "esp_platform": self.esp_platform, - "board": self.board, + "esp_platform": self.target_platform, "build_path": self.build_path, "firmware_bin_path": self.firmware_bin_path, "loaded_integrations": self.loaded_integrations, } def to_json(self): - return json.dumps(self.as_dict(), indent=2) + "\n" + return f"{json.dumps(self.as_dict(), indent=2)}\n" def save(self, path): write_file_if_changed(path, self.to_json()) @@ -110,28 +100,24 @@ class StorageJSON: comment=esph.comment, esphome_version=const.__version__, src_version=1, - arduino_version=esph.arduino_version, address=esph.address, - esp_platform=esph.esp_platform, - board=esph.board, + target_platform=esph.target_platform, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, loaded_integrations=list(esph.loaded_integrations), ) @staticmethod - def from_wizard(name, address, esp_platform, board): - # type: (str, str, str, str) -> StorageJSON + def from_wizard(name, address, esp_platform): + # type: (str, str, str) -> StorageJSON return StorageJSON( storage_version=1, name=name, comment=None, esphome_version=const.__version__, src_version=1, - arduino_version=None, address=address, - esp_platform=esp_platform, - board=board, + target_platform=esp_platform, build_path=None, firmware_bin_path=None, loaded_integrations=[], @@ -148,10 +134,8 @@ class StorageJSON: "esphome_version", storage.get("esphomeyaml_version") ) src_version = storage.get("src_version") - arduino_version = storage.get("arduino_version") address = storage.get("address") esp_platform = storage.get("esp_platform") - board = storage.get("board") build_path = storage.get("build_path") firmware_bin_path = storage.get("firmware_bin_path") loaded_integrations = storage.get("loaded_integrations", []) @@ -161,10 +145,8 @@ class StorageJSON: comment, esphome_version, src_version, - arduino_version, address, esp_platform, - board, build_path, firmware_bin_path, loaded_integrations, @@ -215,7 +197,7 @@ class EsphomeStorageJSON: self.last_update_check_str = new.strftime("%Y-%m-%dT%H:%M:%S") def to_json(self): # type: () -> dict - return json.dumps(self.as_dict(), indent=2) + "\n" + return f"{json.dumps(self.as_dict(), indent=2)}\n" def save(self, path): # type: (str) -> None write_file_if_changed(path, self.to_json()) diff --git a/esphome/types.py b/esphome/types.py new file mode 100644 index 0000000000..6bbfb00ce6 --- /dev/null +++ b/esphome/types.py @@ -0,0 +1,18 @@ +"""This helper module tracks commonly used types in the esphome python codebase.""" +from typing import Dict, Union, List + +from esphome.core import ID, Lambda, EsphomeCore + +ConfigFragmentType = Union[ + str, + int, + float, + None, + Dict[Union[str, int], "ConfigFragmentType"], + List["ConfigFragmentType"], + ID, + Lambda, +] +ConfigType = Dict[str, ConfigFragmentType] +CoreType = EsphomeCore +ConfigPathType = Union[str, int] diff --git a/esphome/util.py b/esphome/util.py index 10f9923c44..527e370ad8 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -247,17 +247,24 @@ class OrderedDict(collections.OrderedDict): return dict(self).__repr__() -def list_yaml_files(folder): - files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) +def list_yaml_files(folders): + files = filter_yaml_files( + [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] + ) files.sort() return files def filter_yaml_files(files): - files = [f for f in files if os.path.splitext(f)[1] == ".yaml"] - files = [f for f in files if os.path.basename(f) != "secrets.yaml"] - files = [f for f in files if not os.path.basename(f).startswith(".")] - return files + return [ + f + for f in files + if ( + os.path.splitext(f)[1] in (".yaml", ".yml") + and os.path.basename(f) not in ("secrets.yaml", "secrets.yml") + and not os.path.basename(f).startswith(".") + ) + ] class SerialPort: diff --git a/esphome/vscode.py b/esphome/vscode.py index 6e1a0270be..68d59abd02 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -67,7 +67,7 @@ def read_config(args): CORE.ace = args.ace f = data["file"] if CORE.ace: - CORE.config_path = os.path.join(args.configuration[0], f) + CORE.config_path = os.path.join(args.configuration, f) else: CORE.config_path = data["file"] vs = VSCodeResult() diff --git a/esphome/wizard.py b/esphome/wizard.py index 0d912e4bbf..5c35fac73a 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -10,7 +10,6 @@ from esphome.helpers import get_bool_env, write_file from esphome.log import color, Fore # pylint: disable=anomalous-backslash-in-string -from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD @@ -74,19 +73,20 @@ def wizard_file(**kwargs): # Configure API if "password" in kwargs: - config += ' password: "{0}"\n'.format(kwargs["password"]) + config += f" password: \"{kwargs['password']}\"\n" # Configure OTA config += "\nota:\n" if "ota_password" in kwargs: - config += ' password: "{0}"'.format(kwargs["ota_password"]) + config += f" password: \"{kwargs['ota_password']}\"" elif "password" in kwargs: - config += ' password: "{0}"'.format(kwargs["password"]) + config += f" password: \"{kwargs['password']}\"" # Configuring wifi config += "\n\nwifi:\n" if "ssid" in kwargs: + # pylint: disable=consider-using-f-string config += """ ssid: "{ssid}" password: "{psk}" """.format( @@ -99,6 +99,7 @@ def wizard_file(**kwargs): networks: """ + # pylint: disable=consider-using-f-string config += """ # Enable fallback hotspot (captive portal) in case wifi connection fails ap: @@ -114,6 +115,8 @@ captive_portal: def wizard_write(path, **kwargs): + from esphome.components.esp8266 import boards as esp8266_boards + name = kwargs["name"] board = kwargs["board"] @@ -122,11 +125,13 @@ def wizard_write(path, **kwargs): kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: - kwargs["platform"] = "ESP8266" if board in ESP8266_BOARD_PINS else "ESP32" + kwargs["platform"] = ( + "ESP8266" if board in esp8266_boards.ESP8266_BOARD_PINS else "ESP32" + ) platform = kwargs["platform"] write_file(path, wizard_file(**kwargs)) - storage = StorageJSON.from_wizard(name, name + ".local", platform, board) + storage = StorageJSON.from_wizard(name, f"{name}.local", platform) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) @@ -166,16 +171,17 @@ def strip_accents(value): def wizard(path): + from esphome.components.esp32 import boards as esp32_boards + from esphome.components.esp8266 import boards as esp8266_boards + if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( - "Please make your configuration file {} have the extension .yaml or .yml" - "".format(color(Fore.CYAN, path)) + f"Please make your configuration file {color(Fore.CYAN, path)} have the extension .yaml or .yml" ) return 1 if os.path.exists(path): safe_print( - "Uh oh, it seems like {} already exists, please delete that file first " - "or chose another configuration file.".format(color(Fore.CYAN, 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 safe_print("Hi there!") @@ -191,17 +197,13 @@ def wizard(path): sleep(3.0) safe_print() safe_print_step(1, CORE_BIG) - safe_print( - "First up, please choose a " + color(Fore.GREEN, "name") + " for your node." - ) + safe_print(f"First up, please choose a {color(Fore.GREEN, 'name')} for your node.") safe_print( "It should be a unique name that can be used to identify the device later." ) sleep(1) safe_print( - "For example, I like calling the node in my living room {}.".format( - color(Fore.BOLD_WHITE, "livingroom") - ) + f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}." ) safe_print() sleep(1) @@ -222,13 +224,11 @@ def wizard(path): name = strip_accents(name).lower().replace(" ", "-") name = strip_accents(name).lower().replace("_", "-") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) - safe_print( - 'Shall I use "{}" as the name instead?'.format(color(Fore.CYAN, name)) - ) + safe_print(f'Shall I use "{color(Fore.CYAN, name)}" as the name instead?') sleep(0.5) name = default_input("(name [{}]): ", name) - safe_print('Great! Your node is now called "{}".'.format(color(Fore.CYAN, name))) + safe_print(f'Great! Your node is now called "{color(Fore.CYAN, name)}".') sleep(1) safe_print_step(2, ESP_BIG) safe_print( @@ -236,11 +236,7 @@ def wizard(path): "firmwares for it." ) safe_print( - "Are you using an " - + color(Fore.GREEN, "ESP32") - + " or " - + color(Fore.GREEN, "ESP8266") - + " platform? (Choose ESP8266 for Sonoff devices)" + f"Are you using an {color(Fore.GREEN, 'ESP32')} or {color(Fore.GREEN, 'ESP8266')} platform? (Choose ESP8266 for Sonoff devices)" ) while True: sleep(0.5) @@ -252,12 +248,9 @@ def wizard(path): break except vol.Invalid: safe_print( - "Unfortunately, I can't find an espressif microcontroller called " - '"{}". Please try again.'.format(platform) + f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.' ) - safe_print( - "Thanks! You've chosen {} as your platform.".format(color(Fore.CYAN, platform)) - ) + safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.") safe_print() sleep(1) @@ -270,24 +263,20 @@ def wizard(path): "http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" ) - safe_print( - "Next, I need to know what " + color(Fore.GREEN, "board") + " you're using." - ) + safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.") sleep(0.5) - safe_print( - "Please go to {} and choose a board.".format(color(Fore.GREEN, board_link)) - ) + safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.") if platform == "ESP32": - safe_print("(Type " + color(Fore.GREEN, "esp01_1m") + " for Sonoff devices)") + safe_print(f"(Type {color(Fore.GREEN, 'esp01_1m')} for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": - safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "nodemcu-32s"))) - boards = list(ESP32_BOARD_PINS.keys()) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".") + boards = list(esp32_boards.ESP32_BOARD_PINS.keys()) else: - safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "nodemcuv2"))) - boards = list(ESP8266_BOARD_PINS.keys()) - safe_print("Options: {}".format(", ".join(sorted(boards)))) + 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))}") while True: board = input(color(Fore.BOLD_WHITE, "(board): ")) @@ -302,9 +291,7 @@ def wizard(path): sleep(0.25) safe_print() - safe_print( - "Way to go! You've chosen {} as your board.".format(color(Fore.CYAN, board)) - ) + safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.") safe_print() sleep(1) @@ -313,12 +300,10 @@ def wizard(path): safe_print() sleep(1) safe_print( - "First, what's the " - + color(Fore.GREEN, "SSID") - + f" (the name) of the WiFi network {name} should connect to?" + f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" ) sleep(1.5) - safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "Abraham Linksys"))) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") while True: ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) try: @@ -328,27 +313,23 @@ def wizard(path): safe_print( color( Fore.RED, - 'Unfortunately, "{}" doesn\'t seem to be a valid SSID. ' - "Please try again.".format(ssid), + f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.', ) ) safe_print() sleep(1) safe_print( - 'Thank you very much! You\'ve just chosen "{}" as your SSID.' - "".format(color(Fore.CYAN, ssid)) + f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.' ) safe_print() sleep(0.75) safe_print( - "Now please state the " - + color(Fore.GREEN, "password") - + " of the WiFi network so that I can connect to it (Leave empty for no password)" + 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('For example "{}"'.format(color(Fore.BOLD_WHITE, "PASSWORD42"))) + safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") sleep(0.5) psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) safe_print( @@ -362,8 +343,7 @@ def wizard(path): "(over the air) and integrates into Home Assistant with a native API." ) safe_print( - "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?" + 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) diff --git a/esphome/writer.py b/esphome/writer.py index 57698f8c25..29532d4f64 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -2,17 +2,13 @@ import logging import os import re from pathlib import Path -from typing import Dict +from typing import Dict, List, Union from esphome.config import iter_components from esphome.const import ( - CONF_BOARD_FLASH_MODE, - CONF_ESPHOME, - CONF_PLATFORMIO_OPTIONS, HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__, - ARDUINO_VERSION_ESP8266, ENV_NOGITIGNORE, ) from esphome.core import CORE, EsphomeError @@ -25,7 +21,6 @@ from esphome.helpers import ( get_bool_env, ) from esphome.storage_json import StorageJSON, storage_path -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS from esphome import loader _LOGGER = logging.getLogger(__name__) @@ -98,7 +93,7 @@ def get_include_text(): includes = "\n".join(includes) if not includes: continue - include_text += includes + "\n" + include_text += f"{includes}\n" return include_text @@ -136,7 +131,7 @@ def migrate_src_version_0_to_1(): content, count = replace_file_content( content, r'#include "esphomelib/application.h"', - CPP_INCLUDE_BEGIN + "\n" + CPP_INCLUDE_END, + f"{CPP_INCLUDE_BEGIN}\n{CPP_INCLUDE_END}", ) if count == 0: _LOGGER.error( @@ -168,10 +163,6 @@ def storage_should_clean(old, new): # type: (StorageJSON, StorageJSON) -> bool if old.src_version != new.src_version: return True - if old.arduino_version != new.arduino_version: - return True - if old.board != new.board: - return True if old.build_path != new.build_path: return True return False @@ -194,10 +185,10 @@ def update_storage_json(): new.save(path) -def format_ini(data): +def format_ini(data: Dict[str, Union[str, List[str]]]) -> str: content = "" for key, value in sorted(data.items()): - if isinstance(value, (list, set, tuple)): + if isinstance(value, list): content += f"{key} =\n" for x in value: content += f" {x}\n" @@ -206,79 +197,15 @@ def format_ini(data): return content -def gather_lib_deps(): - return [x.as_lib_dep for x in CORE.libraries] - - -def gather_build_flags(): - build_flags = CORE.build_flags - - # avoid changing build flags order - return list(sorted(list(build_flags))) - - -ESP32_LARGE_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 -""" - - def get_ini_content(): - lib_deps = gather_lib_deps() - build_flags = gather_build_flags() - - data = { - "platform": CORE.arduino_version, - "board": CORE.board, - "framework": "arduino", - "lib_deps": lib_deps + ["${common.lib_deps}"], - "build_flags": build_flags + ["${common.build_flags}"], - "upload_speed": UPLOAD_SPEED_OVERRIDE.get(CORE.board, 115200), - } - - if CORE.is_esp32: - data["board_build.partitions"] = "partitions.csv" - partitions_csv = CORE.relative_build_path("partitions.csv") - write_file_if_changed(partitions_csv, ESP32_LARGE_PARTITIONS_CSV) - - # pylint: disable=unsubscriptable-object - if CONF_BOARD_FLASH_MODE in CORE.config[CONF_ESPHOME]: - flash_mode = CORE.config[CONF_ESPHOME][CONF_BOARD_FLASH_MODE] - data["board_build.flash_mode"] = flash_mode - - # Build flags - if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES: - flash_size = ESP8266_FLASH_SIZES[CORE.board] - ld_scripts = ESP8266_LD_SCRIPTS[flash_size] - - versions_with_old_ldscripts = [ - ARDUINO_VERSION_ESP8266["2.4.0"], - ARDUINO_VERSION_ESP8266["2.4.1"], - ARDUINO_VERSION_ESP8266["2.4.2"], - ] - if CORE.arduino_version == ARDUINO_VERSION_ESP8266["2.3.0"]: - # No ld script support - ld_script = None - if CORE.arduino_version in versions_with_old_ldscripts: - # Old ld script path - ld_script = ld_scripts[0] - else: - ld_script = ld_scripts[1] - - if ld_script is not None: - data["board_build.ldscript"] = ld_script - - # Ignore libraries that are not explicitly used, but may - # be added by LDF - # data['lib_ldf_mode'] = 'chain' - data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) + CORE.add_platformio_option( + "lib_deps", [x.as_lib_dep for x in CORE.libraries] + ["${common.lib_deps}"] + ) + # Sort to avoid changing build flags order + CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) content = f"[env:{CORE.name}]\n" - content += format_ini(data) + content += format_ini(CORE.platformio_options) return content @@ -321,7 +248,7 @@ def write_platformio_ini(content): ) else: content_format = INI_BASE_FORMAT - full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + "\n" + content + full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" full_file += INI_AUTO_GENERATE_END + content_format[1] write_file_if_changed(path, full_file) @@ -341,7 +268,9 @@ DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ """ VERSION_H_FORMAT = """\ #pragma once +#include "esphome/core/macros.h" #define ESPHOME_VERSION "{}" +#define ESPHOME_VERSION_CODE VERSION_CODE({}, {}, {}) """ DEFINES_H_TARGET = "esphome/core/defines.h" VERSION_H_TARGET = "esphome/core/version.h" @@ -358,12 +287,15 @@ or use the custom_components folder. def copy_src_tree(): - source_files: Dict[Path, loader.SourceFile] = {} + source_files: List[loader.FileResource] = [] for _, component, _ in iter_components(CORE.config): - source_files.update(component.source_files) + source_files += component.resources + source_files_map = { + Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files + } # Convert to list and sort - source_files_l = list(source_files.items()) + source_files_l = list(source_files_map.items()) source_files_l.sort() # Build #include list for esphome.h @@ -374,7 +306,7 @@ def copy_src_tree(): include_l.append("") include_s = "\n".join(include_l) - source_files_copy = source_files.copy() + source_files_copy = source_files_map.copy() ignore_targets = [Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET)] for t in ignore_targets: source_files_copy.pop(t) @@ -414,10 +346,14 @@ def copy_src_tree(): CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s) ) write_file_if_changed( - CORE.relative_src_path("esphome", "core", "version.h"), - VERSION_H_FORMAT.format(__version__), + CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h() ) + if CORE.is_esp32: + from esphome.components.esp32 import copy_files + + copy_files() + def generate_defines_h(): define_content_l = [x.as_macro for x in CORE.defines] @@ -425,6 +361,15 @@ def generate_defines_h(): return DEFINES_H_FORMAT.format("\n".join(define_content_l)) +def generate_version_h(): + match = re.match(r"^(\d+)\.(\d+).(\d+)-?\w*$", __version__) + if not match: + raise EsphomeError(f"Could not parse version {__version__}.") + return VERSION_H_FORMAT.format( + __version__, match.group(1), match.group(2), match.group(3) + ) + + def write_cpp(code_s): path = CORE.relative_src_path("main.cpp") if os.path.isfile(path): @@ -443,9 +388,9 @@ def write_cpp(code_s): global_s = '#include "esphome.h"\n' global_s += CORE.cpp_global_section - full_file = code_format[0] + CPP_INCLUDE_BEGIN + "\n" + global_s + CPP_INCLUDE_END + full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" full_file += ( - code_format[1] + CPP_AUTO_GENERATE_BEGIN + "\n" + code_s + CPP_AUTO_GENERATE_END + f"{code_format[1] + CPP_AUTO_GENERATE_BEGIN}\n{code_s}{CPP_AUTO_GENERATE_END}" ) full_file += code_format[2] write_file_if_changed(path, full_file) @@ -480,5 +425,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome def write_gitignore(): path = CORE.relative_config_path(".gitignore") if not os.path.isfile(path): - with open(path, "w") as f: + with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f98bb272b8..bdadbbd43a 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -183,9 +183,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors raise yaml.constructor.ConstructorError( "While constructing a mapping", node.start_mark, - "Expected a mapping for merging, but found {}".format( - type(item) - ), + f"Expected a mapping for merging, but found {type(item)}", value_node.start_mark, ) merge_pairs.extend(item.items()) @@ -193,8 +191,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors raise yaml.constructor.ConstructorError( "While constructing a mapping", node.start_mark, - "Expected a mapping or list of mappings for merging, " - "but found {}".format(type(value)), + f"Expected a mapping or list of mappings for merging, but found {type(value)}", value_node.start_mark, ) @@ -411,17 +408,19 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:str", value=str(value)) - # pylint: disable=arguments-differ + # pylint: disable=arguments-renamed def represent_bool(self, value): return self.represent_scalar( "tag:yaml.org,2002:bool", "true" if value else "false" ) + # pylint: disable=arguments-renamed def represent_int(self, value): if is_secret(value): return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:int", value=str(value)) + # pylint: disable=arguments-renamed def represent_float(self, value): if is_secret(value): return self.represent_secret(value) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index a44c7c9114..a19fc143ec 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,520 +1,33 @@ -# Custom zeroconf implementation based on python-zeroconf -# (https://github.com/jstasiak/python-zeroconf) that supports Python 2 - -import errno -import logging -import select import socket -import struct -import sys import threading import time +from typing import Dict, Optional +import logging +from dataclasses import dataclass -import ifaddr - -log = logging.getLogger(__name__) - -# Some timing constants - -_LISTENER_TIME = 200 - -# Some DNS constants - -_MDNS_ADDR = "224.0.0.251" -_MDNS_PORT = 5353 - -_MAX_MSG_ABSOLUTE = 8966 - -_FLAGS_QR_MASK = 0x8000 # query response mask -_FLAGS_QR_QUERY = 0x0000 # query -_FLAGS_QR_RESPONSE = 0x8000 # response - -_FLAGS_AA = 0x0400 # Authoritative answer -_FLAGS_TC = 0x0200 # Truncated -_FLAGS_RD = 0x0100 # Recursion desired -_FLAGS_RA = 0x8000 # Recursion available - -_FLAGS_Z = 0x0040 # Zero -_FLAGS_AD = 0x0020 # Authentic data -_FLAGS_CD = 0x0010 # Checking disabled +from zeroconf import ( + DNSAddress, + DNSOutgoing, + DNSRecord, + DNSQuestion, + RecordUpdateListener, + Zeroconf, + ServiceBrowser, +) +from zeroconf._services import ServiceStateChange _CLASS_IN = 1 -_CLASS_CS = 2 -_CLASS_CH = 3 -_CLASS_HS = 4 -_CLASS_NONE = 254 -_CLASS_ANY = 255 -_CLASS_MASK = 0x7FFF -_CLASS_UNIQUE = 0x8000 - +_FLAGS_QR_QUERY = 0x0000 # query _TYPE_A = 1 -_TYPE_NS = 2 -_TYPE_MD = 3 -_TYPE_MF = 4 -_TYPE_CNAME = 5 -_TYPE_SOA = 6 -_TYPE_MB = 7 -_TYPE_MG = 8 -_TYPE_MR = 9 -_TYPE_NULL = 10 -_TYPE_WKS = 11 -_TYPE_PTR = 12 -_TYPE_HINFO = 13 -_TYPE_MINFO = 14 -_TYPE_MX = 15 -_TYPE_TXT = 16 -_TYPE_AAAA = 28 -_TYPE_SRV = 33 -_TYPE_ANY = 255 - -# Mapping constants to names -int2byte = struct.Struct(">B").pack - - -# Exceptions -class Error(Exception): - pass - - -class IncomingDecodeError(Error): - pass - - -# pylint: disable=no-init -class QuietLogger: - _seen_logs = {} - - @classmethod - def log_exception_warning(cls, logger_data=None): - exc_info = sys.exc_info() - exc_str = str(exc_info[1]) - if exc_str not in cls._seen_logs: - # log at warning level the first time this is seen - cls._seen_logs[exc_str] = exc_info - logger = log.warning - else: - logger = log.debug - if logger_data is not None: - logger(*logger_data) - logger("Exception occurred:", exc_info=True) - - @classmethod - def log_warning_once(cls, *args): - msg_str = args[0] - if msg_str not in cls._seen_logs: - cls._seen_logs[msg_str] = 0 - logger = log.warning - else: - logger = log.debug - cls._seen_logs[msg_str] += 1 - logger(*args) - - -class DNSEntry: - """A DNS entry""" - - def __init__(self, name, type_, class_): - self.key = name.lower() - self.name = name - self.type = type_ - self.class_ = class_ & _CLASS_MASK - self.unique = (class_ & _CLASS_UNIQUE) != 0 - - -class DNSQuestion(DNSEntry): - """A DNS question entry""" - - def __init__(self, name, type_, class_): - DNSEntry.__init__(self, name, type_, class_) - - def answered_by(self, rec): - """Returns true if the question is answered by the record""" - return ( - self.class_ == rec.class_ - and (self.type == rec.type or self.type == _TYPE_ANY) - and self.name == rec.name - ) - - -class DNSRecord(DNSEntry): - """A DNS record - like a DNS entry, but has a TTL""" - - def __init__(self, name, type_, class_, ttl): - DNSEntry.__init__(self, name, type_, class_) - self.ttl = 15 - self.created = time.time() - - def write(self, out): - """Abstract method""" - raise NotImplementedError - - def is_expired(self, now): - return self.created + self.ttl <= now - - def is_removable(self, now): - return self.created + self.ttl * 2 <= now - - -class DNSAddress(DNSRecord): - """A DNS address record""" - - def __init__(self, name, type_, class_, ttl, address): - DNSRecord.__init__(self, name, type_, class_, ttl) - self.address = address - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.address) - - -class DNSText(DNSRecord): - """A DNS text record""" - - def __init__(self, name, type_, class_, ttl, text): - assert isinstance(text, (bytes, type(None))) - DNSRecord.__init__(self, name, type_, class_, ttl) - self.text = text - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.text) - - -class DNSIncoming(QuietLogger): - """Object representation of an incoming DNS packet""" - - def __init__(self, data): - """Constructor from string holding bytes of packet""" - self.offset = 0 - self.data = data - self.questions = [] - self.answers = [] - self.id = 0 - self.flags = 0 # type: int - self.num_questions = 0 - self.num_answers = 0 - self.num_authorities = 0 - self.num_additionals = 0 - self.valid = False - - try: - self.read_header() - self.read_questions() - self.read_others() - self.valid = True - - except (IndexError, struct.error, IncomingDecodeError): - self.log_exception_warning( - ("Choked at offset %d while unpacking %r", self.offset, data) - ) - - def unpack(self, format_): - length = struct.calcsize(format_) - info = struct.unpack(format_, self.data[self.offset : self.offset + length]) - self.offset += length - return info - - def read_header(self): - """Reads header portion of packet""" - ( - self.id, - self.flags, - self.num_questions, - self.num_answers, - self.num_authorities, - self.num_additionals, - ) = self.unpack(b"!6H") - - def read_questions(self): - """Reads questions section of packet""" - for _ in range(self.num_questions): - name = self.read_name() - type_, class_ = self.unpack(b"!HH") - - question = DNSQuestion(name, type_, class_) - self.questions.append(question) - - def read_character_string(self): - """Reads a character string from the packet""" - length = self.data[self.offset] - self.offset += 1 - return self.read_string(length) - - def read_string(self, length): - """Reads a string of a given length from the packet""" - info = self.data[self.offset : self.offset + length] - self.offset += length - return info - - def read_unsigned_short(self): - """Reads an unsigned short from the packet""" - return self.unpack(b"!H")[0] - - def read_others(self): - """Reads the answers, authorities and additionals section of the - packet""" - n = self.num_answers + self.num_authorities + self.num_additionals - for _ in range(n): - domain = self.read_name() - type_, class_, ttl, length = self.unpack(b"!HHiH") - - rec = None - if type_ == _TYPE_A: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(4)) - elif type_ == _TYPE_TXT: - rec = DNSText(domain, type_, class_, ttl, self.read_string(length)) - elif type_ == _TYPE_AAAA: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(16)) - else: - # Try to ignore types we don't know about - # Skip the payload for the resource record so the next - # records can be parsed correctly - self.offset += length - - if rec is not None: - self.answers.append(rec) - - def is_query(self): - """Returns true if this is a query""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY - - def is_response(self): - """Returns true if this is a response""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - - def read_utf(self, offset, length): - """Reads a UTF-8 string of a given length from the packet""" - return str(self.data[offset : offset + length], "utf-8", "replace") - - def read_name(self): - """Reads a domain name from the packet""" - result = "" - off = self.offset - next_ = -1 - first = off - - while True: - length = self.data[off] - off += 1 - if length == 0: - break - t = length & 0xC0 - if t == 0x00: - result = "".join((result, self.read_utf(off, length) + ".")) - off += length - elif t == 0xC0: - if next_ < 0: - next_ = off + 1 - off = ((length & 0x3F) << 8) | self.data[off] - if off >= first: - raise IncomingDecodeError(f"Bad domain name (circular) at {off}") - first = off - else: - raise IncomingDecodeError(f"Bad domain name at {off}") - - if next_ >= 0: - self.offset = next_ - else: - self.offset = off - - return result - - -class DNSOutgoing: - """Object representation of an outgoing packet""" - - def __init__(self, flags): - self.finished = False - self.id = 0 - self.flags = flags - self.names = {} - self.data = [] - self.size = 12 - self.state = False - - self.questions = [] - self.answers = [] - - def add_question(self, record): - """Adds a question""" - self.questions.append(record) - - def pack(self, format_, value): - self.data.append(struct.pack(format_, value)) - self.size += struct.calcsize(format_) - - def write_byte(self, value): - """Writes a single byte to the packet""" - self.pack(b"!c", int2byte(value)) - - def insert_short(self, index, value): - """Inserts an unsigned short in a certain position in the packet""" - self.data.insert(index, struct.pack(b"!H", value)) - self.size += 2 - - def write_short(self, value): - """Writes an unsigned short to the packet""" - self.pack(b"!H", value) - - def write_int(self, value): - """Writes an unsigned integer to the packet""" - self.pack(b"!I", int(value)) - - def write_string(self, value): - """Writes a string to the packet""" - assert isinstance(value, bytes) - self.data.append(value) - self.size += len(value) - - def write_utf(self, s): - """Writes a UTF-8 string of a given length to the packet""" - utfstr = s.encode("utf-8") - length = len(utfstr) - self.write_byte(length) - self.write_string(utfstr) - - def write_character_string(self, value): - assert isinstance(value, bytes) - length = len(value) - self.write_byte(length) - self.write_string(value) - - def write_name(self, name): - # split name into each label - parts = name.split(".") - if not parts[-1]: - parts.pop() - - # construct each suffix - name_suffices = [".".join(parts[i:]) for i in range(len(parts))] - - # look for an existing name or suffix - for count, sub_name in enumerate(name_suffices): - if sub_name in self.names: - break - else: - count = len(name_suffices) - - # note the new names we are saving into the packet - name_length = len(name.encode("utf-8")) - for suffix in name_suffices[:count]: - self.names[suffix] = ( - self.size + name_length - len(suffix.encode("utf-8")) - 1 - ) - - # write the new names out. - for part in parts[:count]: - self.write_utf(part) - - # if we wrote part of the name, create a pointer to the rest - if count != len(name_suffices): - # Found substring in packet, create pointer - index = self.names[name_suffices[count]] - self.write_byte((index >> 8) | 0xC0) - self.write_byte(index & 0xFF) - else: - # this is the end of a name - self.write_byte(0) - - def write_question(self, question): - self.write_name(question.name) - self.write_short(question.type) - self.write_short(question.class_) - - def packet(self): - if not self.state: - for question in self.questions: - self.write_question(question) - self.state = True - - self.insert_short(0, 0) # num additionals - self.insert_short(0, 0) # num authorities - self.insert_short(0, 0) # num answers - self.insert_short(0, len(self.questions)) - self.insert_short(0, self.flags) # _FLAGS_QR_QUERY - self.insert_short(0, 0) - return b"".join(self.data) - - -class Engine(threading.Thread): - def __init__(self, zc): - threading.Thread.__init__(self, name="zeroconf-Engine") - self.daemon = True - self.zc = zc - self.readers = {} - self.timeout = 5 - self.condition = threading.Condition() - self.start() - - def run(self): - while not self.zc.done: - # pylint: disable=len-as-condition - with self.condition: - rs = self.readers.keys() - if len(rs) == 0: - # No sockets to manage, but we wait for the timeout - # or addition of a socket - self.condition.wait(self.timeout) - - if len(rs) != 0: - try: - rr, _, _ = select.select(rs, [], [], self.timeout) - if not self.zc.done: - for socket_ in rr: - reader = self.readers.get(socket_) - if reader: - reader.handle_read(socket_) - - except OSError as e: - # If the socket was closed by another thread, during - # shutdown, ignore it and exit - if e.args[0] != socket.EBADF or not self.zc.done: - raise - - def add_reader(self, reader, socket_): - with self.condition: - self.readers[socket_] = reader - self.condition.notify() - - def del_reader(self, socket_): - with self.condition: - del self.readers[socket_] - self.condition.notify() - - -class Listener(QuietLogger): - def __init__(self, zc): - self.zc = zc - self.data = None - - def handle_read(self, socket_): - try: - data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE) - except Exception: # pylint: disable=broad-except - self.log_exception_warning() - return - - log.debug("Received from %r:%r: %r ", addr, port, data) - - self.data = data - msg = DNSIncoming(data) - if not msg.valid or msg.is_query(): - pass - else: - self.zc.handle_response(msg) - - -class RecordUpdateListener: - def update_record(self, zc, now, record): - raise NotImplementedError() +_LOGGER = logging.getLogger(__name__) class HostResolver(RecordUpdateListener): - def __init__(self, name): + def __init__(self, name: str): self.name = name - self.address = None + self.address: Optional[bytes] = None - def update_record(self, zc, now, record): + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: if record is None: return if record.type == _TYPE_A: @@ -522,14 +35,14 @@ class HostResolver(RecordUpdateListener): if record.name == self.name: self.address = record.address - def request(self, zc, timeout): + 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) + zc.add_listener(self, None) while self.address is None: if last <= now: # Timeout @@ -541,7 +54,7 @@ class HostResolver(RecordUpdateListener): next_ = now + delay delay *= 2 - zc.wait(min(next_, last) - now) + time.sleep(min(next_, last) - now) now = time.time() finally: zc.remove_listener(self) @@ -549,246 +62,129 @@ class HostResolver(RecordUpdateListener): return True -class DashboardStatus(RecordUpdateListener, threading.Thread): - def __init__(self, zc, on_update): +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 + + def __init__(self, zc: Zeroconf, on_update) -> None: threading.Thread.__init__(self) self.zc = zc - self.query_hosts = set() - self.key_to_host = {} - self.cache = {} + self.query_hosts: set[str] = set() + self.key_to_host: Dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update - def update_record(self, zc, now, record): - if record is None: - return - if record.type in (_TYPE_A, _TYPE_AAAA, _TYPE_TXT): - assert isinstance(record, DNSEntry) - if record.name in self.query_hosts: - self.cache.setdefault(record.name, []).insert(0, record) - self.purge_cache() - - def purge_cache(self): - new_cache = {} - for host, records in self.cache.items(): - if host not in self.query_hosts: - continue - new_records = [rec for rec in records if not rec.is_removable(time.time())] - if new_records: - new_cache[host] = new_records - self.cache = new_cache - self.on_update({key: self.host_status(key) for key in self.key_to_host}) - - def request_query(self, hosts): + 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): + def stop(self) -> None: self.stop_event.set() self.query_event.set() - def host_status(self, key): - return self.key_to_host.get(key) in self.cache + def host_status(self, key: str) -> bool: + entries = self.zc.cache.entries_with_name(key) + if not entries: + return False + now = time.time() * 1000 - def run(self): - self.zc.add_listener(self) + return any( + (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries + ) + + def run(self) -> None: while not self.stop_event.is_set(): - self.purge_cache() + self.on_update( + {key: self.host_status(host) for key, host in self.key_to_host.items()} + ) + now = time.time() * 1000 for host in self.query_hosts: - if all( - record.is_expired(time.time()) - for record in self.cache.get(host, []) + 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() - self.zc.remove_listener(self) -def get_all_addresses(): - return list( - { - addr.ip - for iface in ifaddr.get_adapters() - for addr in iface.ips - if addr.is_IPv4 - and addr.network_prefix != 32 # Host only netmask 255.255.255.255 - } - ) +ESPHOME_SERVICE_TYPE = "_esphomelib._tcp.local." +TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url" +TXT_RECORD_PROJECT_NAME = b"project_name" +TXT_RECORD_PROJECT_VERSION = b"project_version" -def new_socket(): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, "TCP/IP Illustrated, - # Volume 2"), but some BSD-derived systems require - # SO_REUSEPORT to be specified explicitly. Also, not all - # versions of Python have SO_REUSEPORT available. - # Catch OSError and socket.error for kernel versions <3.9 because lacking - # SO_REUSEPORT support. - try: - reuseport = socket.SO_REUSEPORT - except AttributeError: - pass - else: - try: - s.setsockopt(socket.SOL_SOCKET, reuseport, 1) - except OSError as err: - # OSError on python 3, socket.error on python 2 - if err.errno != errno.ENOPROTOOPT: - raise - - # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and - # IP_MULTICAST_LOOP socket options as an unsigned char. - ttl = struct.pack(b"B", 255) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - loop = struct.pack(b"B", 1) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) - - s.bind(("", _MDNS_PORT)) - return s +@dataclass +class DiscoveredImport: + device_name: str + package_import_url: str + project_name: str + project_version: str -class Zeroconf(QuietLogger): - def __init__(self): - # hook for threads - self._GLOBAL_DONE = False +class DashboardImportDiscovery: + def __init__(self, zc: Zeroconf) -> None: + self.zc = zc + self.service_browser = ServiceBrowser( + self.zc, ESPHOME_SERVICE_TYPE, [self._on_update] + ) + self.import_state = {} - self._listen_socket = new_socket() - interfaces = get_all_addresses() + def _on_update( + self, + zeroconf: Zeroconf, + service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: + _LOGGER.debug( + "service_update: type=%s name=%s state_change=%s", + service_type, + name, + state_change, + ) + if service_type != ESPHOME_SERVICE_TYPE: + return + if state_change == ServiceStateChange.Removed: + self.import_state.pop(name, None) - self._respond_sockets = [] + info = zeroconf.get_service_info(service_type, name) + _LOGGER.debug("-> resolved info: %s", info) + if info is None: + return + node_name = name[: -len(ESPHOME_SERVICE_TYPE) - 1] + required_keys = [ + TXT_RECORD_PACKAGE_IMPORT_URL, + TXT_RECORD_PROJECT_NAME, + TXT_RECORD_PROJECT_VERSION, + ] + if any(key not in info.properties for key in required_keys): + # Not a dashboard import device + return - for i in interfaces: - try: - _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i) - self._listen_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value - ) - except OSError as e: - _errno = e.args[0] - if _errno == errno.EADDRINUSE: - log.info( - "Address in use when adding %s to multicast group, " - "it is expected to happen on some systems", - i, - ) - elif _errno == errno.EADDRNOTAVAIL: - log.info( - "Address not available when adding %s to multicast " - "group, it is expected to happen on some systems", - i, - ) - continue - elif _errno == errno.EINVAL: - log.info( - "Interface of %s does not support multicast, " - "it is expected in WSL", - i, - ) - continue + import_url = info.properties[TXT_RECORD_PACKAGE_IMPORT_URL].decode() + project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() + project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() - else: - raise + self.import_state[name] = DiscoveredImport( + device_name=node_name, + package_import_url=import_url, + project_name=project_name, + project_version=project_version, + ) - respond_socket = new_socket() - respond_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(i) - ) + def cancel(self) -> None: + self.service_browser.cancel() - self._respond_sockets.append(respond_socket) - self.listeners = [] - - self.condition = threading.Condition() - - self.engine = Engine(self) - self.listener = Listener(self) - self.engine.add_reader(self.listener, self._listen_socket) - - @property - def done(self): - return self._GLOBAL_DONE - - def wait(self, timeout): - """Calling thread waits for a given number of milliseconds or - until notified.""" - with self.condition: - self.condition.wait(timeout) - - def notify_all(self): - """Notifies all waiting threads""" - with self.condition: - self.condition.notify_all() - - def resolve_host(self, host, timeout=3.0): +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) return None - - def add_listener(self, listener): - self.listeners.append(listener) - self.notify_all() - - def remove_listener(self, listener): - """Removes a listener.""" - try: - self.listeners.remove(listener) - self.notify_all() - except Exception as e: # pylint: disable=broad-except - log.exception("Unknown error, possibly benign: %r", e) - - def update_record(self, now, rec): - """Used to notify listeners of new information that has updated - a record.""" - for listener in self.listeners: - listener.update_record(self, now, rec) - self.notify_all() - - def handle_response(self, msg): - """Deal with incoming response packets. All answers - are held in the cache, and listeners are notified.""" - now = time.time() - for record in msg.answers: - self.update_record(now, record) - - def send(self, out): - """Sends an outgoing packet.""" - packet = out.packet() - log.debug("Sending %r (%d bytes) as %r...", out, len(packet), packet) - for s in self._respond_sockets: - if self._GLOBAL_DONE: - return - try: - bytes_sent = s.sendto(packet, 0, (_MDNS_ADDR, _MDNS_PORT)) - except Exception: # pylint: disable=broad-except - # on send errors, log the exception and keep going - self.log_exception_warning() - else: - if bytes_sent != len(packet): - self.log_warning_once( - "!!! sent %d out of %d bytes to %r" - % (bytes_sent, len(packet), s) - ) - - def close(self): - """Ends the background threads, and prevent this instance from - servicing further queries.""" - if not self._GLOBAL_DONE: - self._GLOBAL_DONE = True - # shutdown recv socket and thread - self.engine.del_reader(self._listen_socket) - self._listen_socket.close() - self.engine.join() - - # shutdown the rest - self.notify_all() - for s in self._respond_sockets: - s.close() diff --git a/platformio.ini b/platformio.ini index ba7724ad24..9cc7477d51 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,58 +1,153 @@ -; This file is so that the C++ files in this repo -; can be edited with IDEs like VSCode or CLion -; with the platformio system +; This PlatformIO project is for development purposes *only*: clang-tidy derives its compilation +; database from here, and IDEs like CLion and VSCode also use it. This does not actually create a +; usable binary. ; It's *not* used during runtime. [platformio] -default_envs = livingroom8266 -src_dir = . -include_dir = include +default_envs = esp8266, esp32, esp32-idf +src_dir = esphome +include_dir = + +[runtime] +; This are the flags as set by the runtime. +build_flags = + -Wno-unused-variable + -Wno-unused-but-set-variable + -Wno-sign-compare + +[clangtidy] +; This are the flags for clang-tidy. +build_flags = + -Wall + -Wunreachable-code + -Wfor-loop-analysis + -Wshadow-field + -Wshadow-field-in-constructor [common] lib_deps = - AsyncMqttClient-esphome@0.8.4 - ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.7 - FastLED@3.3.2 - NeoPixelBus-esphome@2.6.2 - 1655@1.0.2 ; TinyGPSPlus (has name conflict) - 6865@1.0.0 ; TM1651 Battery Display - 6306@1.0.3 ; HM3301 + esphome/noise-c@0.1.3 ; api + makuna/NeoPixelBus@2.6.7 ; neopixelbus build_flags = - -fno-exceptions - -Wno-sign-compare - -Wno-unused-but-set-variable - -Wno-unused-variable - -DCLANG_TIDY -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = - + - + + +<./> + +<../tests/dummy_main.cpp> + +<../.temp/all-include.cpp> + +[common:arduino] +extends = common +lib_deps = + ${common.lib_deps} + ottowinter/AsyncMqttClient-esphome@0.8.4 ; mqtt + ottowinter/ArduinoJson-esphomelib@5.13.3 ; json + esphome/ESPAsyncWebServer-esphome@1.3.0 ; web_server_base + fastled/FastLED@3.3.2 ; fastled_base + mikalhart/TinyGPSPlus@1.0.2 ; gps + freekode/TM1651@1.0.1 ; tm1651 + seeed-studio/Grove - Laser PM2.5 Sensor HM3301@1.0.3 ; hm3301 + glmnet/Dsmr@0.5 ; dsmr + rweather/Crypto@0.2.0 ; dsmr + dudanov/MideaUART@1.1.8 ; midea + tonia/HeatpumpIR@^1.0.15 ; heatpumpir +build_flags = + ${common.build_flags} + -DUSE_ARDUINO + +[common:idf] +extends = common +build_flags = + ${common.build_flags} + -DUSE_ESP_IDF + +[common:esp8266] +extends = common:arduino +; when changing this also copy it to esphome-docker-base images +platform = platformio/espressif8266 @ 3.2.0 +platform_packages = + platformio/framework-arduinoespressif8266 @ ~3.30002.0 -[env:livingroom8266] -; use Arduino framework v2.4.2 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760) -platform = platformio/espressif8266@1.8.0 framework = arduino board = nodemcuv2 lib_deps = - ${common.lib_deps} - ESP8266WiFi - ESPAsyncTCP-esphome@1.2.3 - Update -build_flags = ${common.build_flags} -src_filter = ${common.src_filter} + ${common:arduino.lib_deps} + ESP8266WiFi ; wifi (Arduino built-in) + Update ; ota (Arduino built-in) + ottowinter/ESPAsyncTCP-esphome@1.2.3 ; async_tcp +build_flags = + ${common:arduino.build_flags} + -DUSE_ESP8266 + -DUSE_ESP8266_FRAMEWORK_ARDUINO + +[common:esp32-arduino] +extends = common:arduino +; when changing this also copy it to esphome-docker-base images +platform = platformio/espressif32 @ 3.3.2 +platform_packages = + platformio/framework-arduinoespressif32 @ ~3.10006.0 -[env:livingroom32] -platform = platformio/espressif32@3.2.0 framework = arduino board = nodemcu-32s lib_deps = - ${common.lib_deps} - esphome/AsyncTCP-esphome@1.2.2 - Update + ${common:arduino.lib_deps} + esphome/AsyncTCP-esphome@1.2.2 ; async_tcp build_flags = - ${common.build_flags} - -DUSE_ETHERNET -src_filter = - ${common.src_filter} - - + ${common:arduino.build_flags} + -DUSE_ESP32 + -DUSE_ESP32_FRAMEWORK_ARDUINO + +[common:esp32-idf] +extends = common:idf +; when changing this also copy it to esphome-docker-base images +platform = platformio/espressif32 @ 3.3.2 +platform_packages = + platformio/framework-espidf @ ~3.40300.0 + +framework = espidf +board = nodemcu-32s +lib_deps = + ${common:idf.lib_deps} + espressif/esp32-camera@1.0.0 ; esp32_camera +build_flags = + ${common:idf.build_flags} + -Wno-nonnull-compare + -DUSE_ESP32 + -DUSE_ESP32_FRAMEWORK_ESP_IDF + +[env:esp8266] +extends = common:esp8266 +build_flags = + ${common:esp8266.build_flags} + ${runtime.build_flags} + +[env:esp8266-tidy] +extends = common:esp8266 +build_flags = + ${common:esp8266.build_flags} + ${clangtidy.build_flags} + +[env:esp32] +extends = common:esp32-arduino +build_flags = + ${common:esp32-arduino.build_flags} + ${runtime.build_flags} + +[env:esp32-tidy] +extends = common:esp32-arduino +build_flags = + ${common:esp32-arduino.build_flags} + ${clangtidy.build_flags} + +[env:esp32-idf] +extends = common:esp32-idf +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf +build_flags = + ${common:esp32-idf.build_flags} + ${runtime.build_flags} + +[env:esp32-idf-tidy] +extends = common:esp32-idf +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${clangtidy.build_flags} diff --git a/requirements.txt b/requirements.txt index e7f69865da..23a00d3755 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,17 @@ -voluptuous==0.12.1 +voluptuous==0.12.2 PyYAML==5.4.1 paho-mqtt==1.5.1 colorama==0.4.4 tornado==6.1 -protobuf==3.17.0 -tzlocal==2.1 -pytz==2021.1 +tzlocal==3.0 # from time +tzdata>=2021.1 # from time pyserial==3.5 -ifaddr==0.1.7 -platformio==5.1.1 -esptool==2.8 -click==7.1.2 -esphome-dashboard==20210611.0 +platformio==5.2.1 +esptool==3.1 +click==8.0.3 +esphome-dashboard==20211011.1 +aioesphomeapi==9.1.5 + +# esp-idf requires this, but doesn't bundle it by default +# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 +kconfiglib==13.7.1 diff --git a/requirements_test.txt b/requirements_test.txt index 15593a8e12..8ebcf24d4d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.8.2 -flake8==3.9.2 -black==21.5b1 +pylint==2.11.1 +flake8==4.0.1 +black==21.9b0 pexpect==4.8.0 pre-commit # Unit tests -pytest==6.2.4 -pytest-cov==2.11.1 -pytest-mock==3.5.1 -pytest-asyncio==0.14.0 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-mock==3.6.1 +pytest-asyncio==0.15.1 asyncmock==0.4.2 -hypothesis==5.21.0 +hypothesis==5.49.0 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py old mode 100644 new mode 100755 index 97cc95e556..7ccdc5a24e --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. @@ -660,8 +661,12 @@ def build_message_type(desc): o += "\n" o += f" {o2}\n" o += "}\n" + cpp += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += o - prot = "void dump_to(std::string &out) const override;" + cpp += f"#endif\n" + prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" + prot += "void dump_to(std::string &out) const override;\n" + prot += "#endif\n" public_content.append(prot) out = f"class {desc.name} : public ProtoMessage {{\n" @@ -773,7 +778,9 @@ def build_service_message_type(mt): hout += f"bool {func}(const {mt.name} &msg);\n" cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n" if log: + cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + cout += f"#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" @@ -787,7 +794,9 @@ def build_service_message_type(mt): case += f"{mt.name} msg;\n" case += f"msg.decode(msg_data, msg_size);\n" if log: + case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + case += f"#endif\n" case += f"this->{func}(msg);\n" if ifdef is not None: case += f"#endif\n" @@ -821,7 +830,7 @@ cpp += """\ namespace esphome { namespace api { -static const char *TAG = "api.service"; +static const char *const TAG = "api.service"; """ diff --git a/script/build_compile_commands.py b/script/build_compile_commands.py deleted file mode 100755 index 4ac14f08b4..0000000000 --- a/script/build_compile_commands.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os.path - -sys.path.append(os.path.dirname(__file__)) -from helpers import build_all_include, build_compile_commands - - -def main(): - build_all_include() - build_compile_commands() - print("Done.") - - -if __name__ == "__main__": - main() diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 89d621fd5a..7a3257411c 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -25,6 +25,11 @@ JSC_DESCRIPTION = "description" JSC_ONEOF = "oneOf" JSC_PROPERTIES = "properties" JSC_REF = "$ref" + +# this should be required, but YAML Language server completion does not work properly if required are specified. +# still needed for other features / checks +JSC_REQUIRED = "required_" + SIMPLE_AUTOMATION = "simple_automation" schema_names = {} @@ -295,9 +300,17 @@ def get_automation_schema(name, vschema): # * an object with automation's schema and a then key # with again a single action or an array of actions + if len(extra_jschema[JSC_PROPERTIES]) == 0: + return get_ref(SIMPLE_AUTOMATION) + extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object( get_ref(JSC_ACTION) ) + # if there is a required element in extra_jschema then this automation does not support + # directly a list of actions + if JSC_REQUIRED in extra_jschema: + return create_ref(name, extra_vschema, extra_jschema) + jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION)) jschema[JSC_ANYOF].append(extra_jschema) @@ -370,9 +383,14 @@ def get_entry(parent_key, vschema): # everything else just accept string and let ESPHome validate try: from esphome.core import ID + from esphome.automation import Trigger, Automation v = vschema(None) if isinstance(v, ID): + if v.type.base != "script::Script" and ( + v.type.inherits_from(Trigger) or v.type == Automation + ): + return None entry = {"type": "string", "id_type": v.type.base} elif isinstance(v, str): entry = {"type": "string"} @@ -419,7 +437,7 @@ def get_jschema(path, vschema, create_return_ref=True): def get_schema_str(vschema): - # Hack on cs.use_id, in the future this can be improved by trackign which type is required by + # Hack on cs.use_id, in the future this can be improved by tracking which type is required by # the id, this information can be added somehow to schema (not supported by jsonschema) and # completion can be improved listing valid ids only Meanwhile it's a problem because it makes # all partial schemas with cv.use_id different, e.g. i2c @@ -494,9 +512,11 @@ def convert_schema(path, vschema, un_extend=True): output = {} if str(vschema) in ejs.hidden_schemas: - # this can get another think twist. When adding this I've already figured out - # interval and script in other way - if path not in ["interval", "script"]: + if ejs.hidden_schemas[str(vschema)] == "automation": + vschema = vschema(ejs.jschema_extractor) + jschema = get_jschema(path, vschema, True) + return add_definition_array_or_single_object(jschema) + else: vschema = vschema(ejs.jschema_extractor) if un_extend: @@ -515,9 +535,8 @@ def convert_schema(path, vschema, un_extend=True): return rhs # merge - if JSC_ALLOF in lhs and JSC_ALLOF in rhs: - output = lhs[JSC_ALLOF] + output = lhs for k in rhs[JSC_ALLOF]: merge(output[JSC_ALLOF], k) elif JSC_ALLOF in lhs: @@ -574,6 +593,7 @@ def convert_schema(path, vschema, un_extend=True): return output props = output[JSC_PROPERTIES] = {} + required = [] output["type"] = ["object", "null"] if DUMP_COMMENTS: @@ -616,13 +636,21 @@ def convert_schema(path, vschema, un_extend=True): if prop: # Deprecated (cv.Invalid) properties not added props[str(k)] = prop # TODO: see required, sometimes completions doesn't show up because of this... - # if isinstance(k, cv.Required): - # required.append(str(k)) + if isinstance(k, cv.Required): + required.append(str(k)) try: if str(k.default) != "...": - prop["default"] = k.default() + default_value = k.default() + # Yaml validator fails if `"default": null` ends up in the json schema + if default_value is not None: + if prop["type"] == "string": + default_value = str(default_value) + prop["default"] = default_value except: pass + + if len(required) > 0: + output[JSC_REQUIRED] = required return output @@ -648,6 +676,7 @@ def add_pin_registry(): internal = definitions[schema_name] definitions[schema_name]["additionalItems"] = False definitions[f"PIN.{mode}_INTERNAL"] = internal + internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]} schemas = [get_ref(f"PIN.{mode}_INTERNAL")] schemas[0]["required"] = ["number"] # accept string and object, for internal shorthand pin IO: @@ -675,7 +704,7 @@ def dump_schema(): # The root directory of the repo root = Path(__file__).parent.parent - # Fake some diretory so that get_component works + # Fake some directory so that get_component works CORE.config_path = str(root) file_path = args.output diff --git a/script/bump-docker-base-version.py b/script/bump-docker-base-version.py deleted file mode 100755 index 765a330ce4..0000000000 --- a/script/bump-docker-base-version.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import re -import sys - - -def sub(path, pattern, repl, expected_count=1): - with open(path) 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: - fh.write(content) - - -def write_version(version: str): - for p in [ - ".github/workflows/ci-docker.yml", - ".github/workflows/release-dev.yml", - ".github/workflows/release.yml", - ]: - sub(p, r'base_version=".*"', f'base_version="{version}"') - - sub( - "docker/Dockerfile", - r"ARG BUILD_FROM=esphome/esphome-base-amd64:.*", - f"ARG BUILD_FROM=esphome/esphome-base-amd64:{version}", - ) - sub( - "docker/Dockerfile.dev", - r"FROM esphome/esphome-base-amd64:.*", - f"FROM esphome/esphome-base-amd64:{version}", - ) - sub( - "docker/Dockerfile.lint", - r"FROM esphome/esphome-lint-base:.*", - f"FROM esphome/esphome-lint-base:{version}", - ) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("new_version", type=str) - args = parser.parse_args() - - version = args.new_version - print(f"Bumping to {version}") - write_version(version) - return 0 - - -if __name__ == "__main__": - sys.exit(main() or 0) diff --git a/script/bump-version.py b/script/bump-version.py index b7b048eb22..1f034344f9 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -50,16 +50,10 @@ def sub(path, pattern, repl, expected_count=1): def write_version(version: Version): - sub( - "esphome/const.py", r"^MAJOR_VERSION = \d+$", f"MAJOR_VERSION = {version.major}" - ) - sub( - "esphome/const.py", r"^MINOR_VERSION = \d+$", f"MINOR_VERSION = {version.minor}" - ) sub( "esphome/const.py", - r"^PATCH_VERSION = .*$", - f'PATCH_VERSION = "{version.full_patch}"', + r"^__version__ = .*$", + f'__version__ = "{version}"', ) diff --git a/script/ci-custom.py b/script/ci-custom.py index 4ec7c664a4..8e9ca487a6 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from helpers import git_ls_files, filter_changed import codecs import collections import fnmatch @@ -12,7 +13,6 @@ import functools import argparse sys.path.append(os.path.dirname(__file__)) -from helpers import git_ls_files, filter_changed def find_all(a_str, sub): @@ -217,7 +217,9 @@ def lint_ext_check(fname): ) -@lint_file_check(exclude=["docker/rootfs/*", "docker/*.py", "script/*", "setup.py"]) +@lint_file_check( + exclude=["**.sh", "docker/hassio-rootfs/**", "docker/*.py", "script/*", "setup.py"] +) def lint_executable_bit(fname): ex = EXECUTABLE_BIT[fname] if ex != 100644: @@ -261,7 +263,7 @@ def highlight(s): @lint_re_check( r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL, include=cpp_include, - exclude=["esphome/core/log.h"], + exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"], ) def lint_no_defines(fname, match): s = highlight( @@ -406,7 +408,6 @@ ARDUINO_FORBIDDEN_RE = r"[^\w\d](" + r"|".join(ARDUINO_FORBIDDEN) + r")\(.*" exclude=[ "esphome/components/mqtt/custom_mqtt_device.h", "esphome/components/sun/sun.cpp", - "esphome/core/esphal.*", ], ) def lint_no_arduino_framework_functions(fname, match): @@ -420,6 +421,28 @@ def lint_no_arduino_framework_functions(fname, match): ) +IDF_CONVERSION_FORBIDDEN = { + "ARDUINO_ARCH_ESP32": "USE_ESP32", + "ARDUINO_ARCH_ESP8266": "USE_ESP8266", + "pgm_read_byte": "progmem_read_byte", + "ICACHE_RAM_ATTR": "IRAM_ATTR", + "esphome/core/esphal.h": "esphome/core/hal.h", +} +IDF_CONVERSION_FORBIDDEN_RE = r"(" + r"|".join(IDF_CONVERSION_FORBIDDEN) + r").*" + + +@lint_re_check( + IDF_CONVERSION_FORBIDDEN_RE, + include=cpp_include, +) +def lint_no_removed_in_idf_conversions(fname, match): + replacement = IDF_CONVERSION_FORBIDDEN[match.group(1)] + return ( + f"The macro {highlight(match.group(1))} can no longer be used in ESPHome directly. " + f"Plese use {highlight(replacement)} instead." + ) + + @lint_re_check( r"[^\w\d]byte\s+[\w\d]+\s*=", include=cpp_include, @@ -493,7 +516,12 @@ def lint_relative_py_import(fname): "esphome/components/*.h", "esphome/components/*.cpp", "esphome/components/*.tcc", - ] + ], + exclude=[ + "esphome/components/socket/headers.h", + "esphome/components/esp32/core.cpp", + "esphome/components/esp8266/core.cpp", + ], ) def lint_namespace(fname, content): expected_name = re.match( @@ -559,15 +587,18 @@ def lint_inclusive_language(fname, match): "esphome/components/display/display_buffer.h", "esphome/components/i2c/i2c.h", "esphome/components/mqtt/mqtt_component.h", + "esphome/components/number/number.h", "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", + "esphome/components/nextion/nextion_base.h", + "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", "esphome/components/text_sensor/text_sensor.h", "esphome/components/climate/climate.h", "esphome/core/component.h", - "esphome/core/esphal.h", + "esphome/core/gpio.h", "esphome/core/log.h", "tests/custom.h", ], diff --git a/script/clang-format b/script/clang-format index bb2b722e1c..d6588f1ccb 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse import multiprocessing import os +import queue import re import subprocess import sys @@ -13,59 +12,47 @@ import threading import click sys.path.append(os.path.dirname(__file__)) -from helpers import basepath, get_output, git_ls_files, filter_changed - -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue - -root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..'))) -basepath = os.path.join(root_path, 'esphome') -rel_basepath = os.path.relpath(basepath, os.getcwd()) +from helpers import get_output, git_ls_files, filter_changed -def run_format(args, queue, lock): - """Takes filenames out of queue and runs clang-tidy on them.""" +def run_format(args, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() invocation = ['clang-format-11'] if args.inplace: invocation.append('-i') + else: + invocation.extend(['--dry-run', '-Werror']) invocation.append(path) - proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, err = proc.communicate() - with lock: - if proc.returncode != 0: - print(' '.join(invocation)) - print(output.decode('utf-8')) - print(err.decode('utf-8')) + proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + if proc.returncode != 0: + with lock: + print() + print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) + print(proc.stdout) + print(proc.stderr) + print() + failed_files.append(path) queue.task_done() def progress_bar_show(value): - if value is None: - return '' - return value + return value if value is not None else '' def main(): parser = argparse.ArgumentParser() parser.add_argument('-j', '--jobs', type=int, default=multiprocessing.cpu_count(), - help='number of tidy instances to be run in parallel.') + help='number of format instances to be run in parallel.') parser.add_argument('files', nargs='*', default=[], help='files to be processed (regex on path)') parser.add_argument('-i', '--inplace', action='store_true', - help='apply fix-its') - parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='reformat files in-place') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') + help='only run on changed files') args = parser.parse_args() try: @@ -75,7 +62,7 @@ def main(): Oops. It looks like clang-format is not installed. Please check you can run "clang-format-11 -version" in your terminal and install - clang-format (v7) if necessary. + clang-format (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-format. @@ -83,28 +70,26 @@ def main(): return 1 files = [] - for path in git_ls_files(): - filetypes = ('.cpp', '.h', '.tcc') - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp', '*.h', '*.tcc']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + 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() - return_code = 0 + failed_files = [] try: 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)) + args=(args, task_queue, lock, failed_files)) t.daemon = True t.start() @@ -122,7 +107,7 @@ def main(): print('Ctrl-C detected, goodbye.') os.kill(0, 9) - sys.exit(return_code) + sys.exit(len(failed_files)) if __name__ == '__main__': diff --git a/script/clang-tidy b/script/clang-tidy index 0bf17f9076..87ba1c84b5 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse +import json import multiprocessing import os +import queue import re import shutil import subprocess @@ -16,41 +16,89 @@ import click import pexpect sys.path.append(os.path.dirname(__file__)) -from helpers import basepath, shlex_quote, get_output, build_compile_commands, \ - build_all_include, temp_header_file, git_ls_files, filter_changed - -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue +from helpers import shlex_quote, get_output, filter_grep, \ + build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, basepath -def run_tidy(args, tmpdir, queue, lock, failed_files): +def clang_options(idedata): + cmd = [ + # target 32-bit arch (this prevents size mismatch errors on a 64-bit host) + '-m32', + # disable built-in include directories from the host + '-nostdinc', + '-nostdinc++', + # replace pgmspace.h, as it uses GNU extensions clang doesn't support + # https://github.com/earlephilhower/newlib-xtensa/pull/18 + '-D_PGMSPACE_H_', + '-Dpgm_read_byte(s)=(*(const uint8_t *)(s))', + '-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))', + '-Dpgm_read_dword(s)=(*(const uint32_t *)(s))', + '-DPROGMEM=', + '-DPGM_P=const char *', + '-DPSTR(s)=(s)', + # this next one is also needed with upstream pgmspace.h + # suppress warning about identifier naming in expansion of this macro + '-DPSTRN(s, n)=(s)', + # suppress warning about attribute cannot be applied to type + # https://github.com/esp8266/Arduino/pull/8258 + '-Ddeprecated(x)=', + # pretend we're an Xtensa compiler, which gates some features in the headers + '-D__XTENSA__', + # allow to condition code on the presence of clang-tidy + '-DCLANG_TIDY', + # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know + '-D__XTENSA_API_H__', + # (esp-idf) Fix __once_callable in some libstdc++ headers + '-D_GLIBCXX_HAVE_TLS', + ] + + # copy compiler flags, except those clang doesn't understand. + cmd.extend(flag for flag in idedata['cxx_flags'].split(' ') + if flag not in ('-free', '-fipa-pta', '-fstrict-volatile-bitfields', + '-mlongcalls', '-mtext-section-literals', + '-mfix-esp32-psram-cache-issue', '-mfix-esp32-psram-cache-strategy=memw')) + + # defines + cmd.extend(f'-D{define}' for define in idedata['defines']) + + # add include directories, using -isystem for dependencies to suppress their errors + for directory in idedata['includes']['toolchain']: + if 'xtensa-esp32s2-elf' not in directory: + cmd.extend(['-isystem', directory]) + for directory in sorted(set(idedata['includes']['build'])): + dependency = "framework-arduino" in directory or "/libdeps/" in directory + cmd.extend(['-isystem' if dependency else '-I', directory]) + + return cmd + + +def run_tidy(args, options, tmpdir, queue, lock, failed_files): while True: path = queue.get() - invocation = ['clang-tidy-11', '-header-filter=^{}/.*'.format(re.escape(basepath))] + invocation = ['clang-tidy-11'] + if tmpdir is not None: - invocation.append('-export-fixes') + invocation.append('--export-fixes') # Get a temporary file. We immediately close the handle so clang-tidy can # overwrite it. (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) os.close(handle) invocation.append(name) - invocation.append('-p=.') + if args.quiet: invocation.append('-quiet') - for arg in ['-Wfor-loop-analysis', '-Wshadow-field', '-Wshadow-field-in-constructor']: - invocation.append('-extra-arg={}'.format(arg)) + invocation.append(os.path.abspath(path)) + invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*") + invocation.append('--') + invocation.extend(options) invocation_s = ' '.join(shlex_quote(x) for x in invocation) # Use pexpect for a pseudy-TTY with colored output output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8', timeout=15 * 60) - with lock: - if rc != 0: + if rc != 0: + with lock: print() print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) print(output) @@ -74,19 +122,22 @@ def main(): parser.add_argument('-j', '--jobs', type=int, default=multiprocessing.cpu_count(), help='number of tidy instances to be run in parallel.') + parser.add_argument('-e', '--environment', default='esp32-tidy', + help='the PlatformIO environment to run against (esp8266-tidy or esp32-tidy)') parser.add_argument('files', nargs='*', default=[], help='files to be processed (regex on path)') parser.add_argument('--fix', action='store_true', help='apply fix-its') parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='run clang-tidy in quiet mode') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') - parser.add_argument('--split-num', type=int, help='Split the files into X jobs.', + help='only run on changed files') + parser.add_argument('-g', '--grep', help='only run on files containing value') + parser.add_argument('--split-num', type=int, help='split the files into X jobs.', default=None) - parser.add_argument('--split-at', type=int, help='Which split is this? Starts at 1', + parser.add_argument('--split-at', type=int, help='which split is this? starts at 1', default=None) parser.add_argument('--all-headers', action='store_true', - help='Create a dummy file that checks all headers') + help='create a dummy file that checks all headers') args = parser.parse_args() try: @@ -103,29 +154,31 @@ def main(): """) return 1 - build_all_include() - build_compile_commands() + idedata = load_idedata(args.environment) + options = clang_options(idedata) files = [] - for path in git_ls_files(): - filetypes = ('.cpp',) - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + 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) + if args.grep: + files = filter_grep(files, args.grep) + files.sort() if args.split_num: files = split_list(files, args.split_num)[args.split_at - 1] if args.all_headers and args.split_at in (None, 1): + build_all_include() files.insert(0, temp_header_file) tmpdir = None @@ -133,13 +186,12 @@ def main(): tmpdir = tempfile.mkdtemp() failed_files = [] - return_code = 0 try: task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread(target=run_tidy, - args=(args, tmpdir, task_queue, lock, failed_files)) + args=(args, options, tmpdir, task_queue, lock, failed_files)) t.daemon = True t.start() @@ -151,7 +203,6 @@ def main(): # Wait for all threads to be done. task_queue.join() - return_code = len(failed_files) except KeyboardInterrupt: print() @@ -168,8 +219,8 @@ def main(): print('Error applying fixes.\n', file=sys.stderr) raise - return return_code + sys.exit(len(failed_files)) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create new file mode 100755 index 0000000000..120ab3307d --- /dev/null +++ b/script/devcontainer-post-create @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e +# set -x + +mkdir -p config +script/setup + +cpp_json=.vscode/c_cpp_properties.json +if [ ! -f $cpp_json ]; then + echo "Initializing PlatformIO..." + pio init --ide vscode --silent + sed -i "/\\/workspaces\/esphome\/include/d" $cpp_json +else + echo "Cpp environment already configured. To reconfigure it you can run one the following commands:" + echo " pio init --ide vscode" +fi diff --git a/script/helpers.py b/script/helpers.py index 5b1b7ba918..430d8a8e7f 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,13 +1,14 @@ import codecs -import json import os.path import re import subprocess -import sys +import json +from pathlib import Path root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") -temp_header_file = os.path.join(root_path, ".temp-clang-tidy.cpp") +temp_folder = os.path.join(root_path, ".temp") +temp_header_file = os.path.join(temp_folder, "all-include.cpp") def shlex_quote(s): @@ -33,63 +34,9 @@ def build_all_include(): headers.sort() headers.append("") content = "\n".join(headers) - with codecs.open(temp_header_file, "w", encoding="utf-8") as f: - f.write(content) - - -def build_compile_commands(): - gcc_flags_json = os.path.join(root_path, ".gcc-flags.json") - if not os.path.isfile(gcc_flags_json): - print("Could not find {} file which is required for clang-tidy.") - print( - 'Please run "pio init --ide atom" in the root esphome folder to generate that file.' - ) - sys.exit(1) - with codecs.open(gcc_flags_json, "r", encoding="utf-8") as f: - gcc_flags = json.load(f) - exec_path = gcc_flags["execPath"] - include_paths = gcc_flags["gccIncludePaths"].split(",") - includes = [f"-I{p}" for p in include_paths] - cpp_flags = gcc_flags["gccDefaultCppFlags"].split(" ") - defines = [flag for flag in cpp_flags if flag.startswith("-D")] - command = [exec_path] - command.extend(includes) - command.extend(defines) - command.append("-std=gnu++11") - command.append("-Wall") - command.append("-Wno-delete-non-virtual-dtor") - command.append("-Wno-unused-variable") - command.append("-Wunreachable-code") - - source_files = [] - for path in walk_files(basepath): - filetypes = (".cpp",) - ext = os.path.splitext(path)[1] - if ext in filetypes: - source_files.append(os.path.abspath(path)) - source_files.append(temp_header_file) - source_files.sort() - compile_commands = [ - { - "directory": root_path, - "command": " ".join( - shlex_quote(x) for x in (command + ["-o", p + ".o", "-c", p]) - ), - "file": p, - } - for p in source_files - ] - compile_commands_json = os.path.join(root_path, "compile_commands.json") - if os.path.isfile(compile_commands_json): - with codecs.open(compile_commands_json, "r", encoding="utf-8") as f: - try: - if json.load(f) == compile_commands: - return - # pylint: disable=bare-except - except: - pass - with codecs.open(compile_commands_json, "w", encoding="utf-8") as f: - json.dump(compile_commands, f, indent=2) + p = Path(temp_header_file) + p.parent.mkdir(exist_ok=True) + p.write_text(content) def walk_files(path): @@ -145,9 +92,55 @@ def filter_changed(files): return files -def git_ls_files(): +def filter_grep(files, value): + matched = [] + for file in files: + with open(file, "r") as handle: + contents = handle.read() + if value in contents: + matched.append(file) + return matched + + +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() lines = [x.split() for x in output.decode("utf-8").splitlines()] return {s[3].strip(): int(s[0]) for s in lines} + + +def load_idedata(environment): + platformio_ini = Path(root_path) / "platformio.ini" + temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" + changed = False + if not platformio_ini.is_file() or not temp_idedata.is_file(): + changed = True + elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + changed = True + + if "idf" in environment: + # remove full sdkconfig when the defaults have changed so that it is regenerated + default_sdkconfig = Path(root_path) / "sdkconfig.defaults" + temp_sdkconfig = Path(temp_folder) / f"sdkconfig-{environment}" + + if not temp_sdkconfig.is_file(): + changed = True + elif default_sdkconfig.stat().st_mtime >= temp_sdkconfig.stat().st_mtime: + temp_sdkconfig.unlink() + changed = True + + if not changed: + return json.loads(temp_idedata.read_text()) + + # ensure temp directory exists before running pio, as it writes sdkconfig to it + Path(temp_folder).mkdir(exist_ok=True) + + stdout = subprocess.check_output(["pio", "run", "-t", "idedata", "-e", environment]) + match = re.search(r'{\s*".*}', stdout.decode("utf-8")) + data = json.loads(match.group()) + + temp_idedata.write_text(json.dumps(data, indent=2) + "\n") + return data diff --git a/script/lint-cpp b/script/lint-cpp index 170d61d539..ac03ca0f23 100755 --- a/script/lint-cpp +++ b/script/lint-cpp @@ -3,9 +3,6 @@ set -e cd "$(dirname "$0")/.." -if [[ ! -e ".gcc-flags.json" ]]; then - pio init --ide atom -fi set -x diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000000..6b2d6f8f2e --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,17 @@ +# ESP-IDF sdkconfig defaults used for development purposes only, not used during runtime. Used when PlatformIO is ran +# directly from the source directory, e.g. by IDEs or for static analysis (clang-tidy). This should enable all flags +# that are set by any component. + +# esp32 +CONFIG_COMPILER_OPTIMIZATION_DEFAULT=n +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_PARTITION_TABLE_CUSTOM=y +#CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_SINGLE_APP=n + +# esp32_ble +CONFIG_BT_ENABLED=y + +# esp32_camera +CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y +CONFIG_ESP32_SPIRAM_SUPPORT=y diff --git a/setup.py b/setup.py index 44a5965887..967eadd70f 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) -DOWNLOAD_URL = "{}/archive/v{}.zip".format(GITHUB_URL, const.__version__) +DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, const.__version__) here = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 8da93a476e..514bc6ee5f 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -30,7 +30,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): # Then assert 'bs_1->set_name("test bs1");' in main_cpp - assert "bs_1->set_pin(new GPIOPin" in main_cpp + assert "bs_1->set_pin(" in main_cpp def test_binary_sensor_config_value_internal_set(generate_main): diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index c3b192d15f..d956387665 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,26 +13,20 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); - auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); + auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); // NOLINT log->pre_setup(); App.register_component(log); - auto *wifi = new wifi::WiFiComponent(); + auto *wifi = new wifi::WiFiComponent(); // NOLINT App.register_component(wifi); wifi::WiFiAP ap; ap.set_ssid("Test SSID"); ap.set_password("password1"); wifi->add_sta(ap); - auto *ota = new ota::OTAComponent(); + auto *ota = new ota::OTAComponent(); // NOLINT ota->set_port(8266); - auto *gpio = new gpio::GPIOSwitch(); - gpio->set_name("GPIO Switch"); - gpio->set_pin(new GPIOPin(8, OUTPUT, false)); - App.register_component(gpio); - App.register_switch(gpio); - App.setup(); } diff --git a/tests/test1.yaml b/tests/test1.yaml index 29df5857d3..157ccfc5d1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -71,7 +71,6 @@ wifi: password: '' channel: 14 bssid: 'A1:63:95:47:D3:1D' - enable_mdns: true manual_ip: static_ip: 192.168.178.230 gateway: 192.168.178.1 @@ -80,7 +79,10 @@ wifi: dns2: 1.2.2.1 domain: .local reboot_timeout: 120s - power_save_mode: none + power_save_mode: light + +mdns: + disabled: false http_request: useragent: esphome/device @@ -129,6 +131,8 @@ mqtt: - 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"]; @@ -168,6 +172,7 @@ i2c: scan: True frequency: 100kHz setup_priority: -100 + id: i2c_bus spi: clk_pin: GPIO21 @@ -175,15 +180,18 @@ spi: miso_pin: GPIO23 uart: - - tx_pin: GPIO22 - rx_pin: GPIO23 + - tx_pin: + number: GPIO22 + inverted: yes + rx_pin: + number: GPIO23 + inverted: yes baud_rate: 115200 id: uart0 parity: NONE data_bits: 8 stop_bits: 1 rx_buffer_size: 512 - invert: false - id: adalight_uart tx_pin: GPIO25 @@ -197,6 +205,24 @@ ota: 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 @@ -226,6 +252,7 @@ deep_sleep: ads1115: address: 0x48 + i2c_id: i2c_bus dallas: pin: GPIO23 @@ -316,6 +343,7 @@ sensor: - exponential_moving_average: alpha: 0.1 send_every: 15 + - throttle_average: 60s - throttle: 1s - heartbeat: 5s - debounce: 0.1s @@ -337,8 +365,11 @@ sensor: then: - lambda: >- ESP_LOGD("main", "Got value range %f", x); + - wait_until: wifi.connected - wait_until: - binary_sensor.is_on: binary_sensor1 + condition: + binary_sensor.is_on: binary_sensor1 + timeout: 1s on_raw_value: - lambda: >- ESP_LOGD("main", "Got raw value %f", x); @@ -416,6 +447,7 @@ sensor: availability: state_topic: livingroom/custom_state_topic measurement_duration: 31 + i2c_id: i2c_bus - platform: bme280 temperature: name: 'Outside Temperature' @@ -429,6 +461,7 @@ sensor: address: 0x77 iir_filter: 16x update_interval: 15s + i2c_id: i2c_bus - platform: bme680 temperature: name: 'Outside Temperature' @@ -444,6 +477,7 @@ sensor: temperature: 320 duration: 150ms update_interval: 15s + i2c_id: i2c_bus - platform: bmp085 temperature: name: 'Outside Temperature' @@ -453,6 +487,7 @@ sensor: - lambda: >- return x / powf(1.0 - (x / 44330.0), 5.255); update_interval: 15s + i2c_id: i2c_bus - platform: bmp280 temperature: name: 'Outside Temperature' @@ -462,6 +497,7 @@ sensor: address: 0x77 update_interval: 15s iir_filter: 16x + i2c_id: i2c_bus - platform: dallas address: 0x1C0000031EDD2A28 name: 'Living Room Temperature' @@ -483,6 +519,7 @@ sensor: humidity: name: 'Living Room Humidity 4' update_interval: 15s + i2c_id: i2c_bus - platform: duty_cycle pin: GPIO25 name: Duty Cycle Sensor @@ -495,6 +532,7 @@ sensor: humidity: name: 'Living Room Pressure 5' update_interval: 15s + i2c_id: i2c_bus - platform: hlw8012 sel_pin: 5 cf_pin: 14 @@ -514,6 +552,7 @@ sensor: 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' @@ -521,6 +560,11 @@ sensor: sensor: hlw8012_power name: 'Integration Sensor' time_unit: s + - platform: integration + sensor: hlw8012_power + name: 'Integration Sensor lazy' + time_unit: s + min_save_interval: 60s - platform: hmc5883l address: 0x68 field_strength_x: @@ -534,6 +578,7 @@ sensor: range: 130uT oversampling: 8x update_interval: 15s + i2c_id: i2c_bus - platform: qmc5883l address: 0x0D field_strength_x: @@ -547,6 +592,7 @@ sensor: range: 800uT oversampling: 256x update_interval: 15s + i2c_id: i2c_bus - platform: hx711 name: 'HX711 Value' dout_pin: GPIO23 @@ -567,6 +613,7 @@ sensor: max_voltage: 32.0V max_current: 3.2A update_interval: 15s + i2c_id: i2c_bus - platform: ina226 address: 0x40 shunt_resistance: 0.1 ohm @@ -580,6 +627,7 @@ sensor: name: 'INA226 Shunt Voltage' max_current: 3.2A update_interval: 15s + i2c_id: i2c_bus - platform: ina3221 address: 0x40 channel_1: @@ -593,12 +641,14 @@ sensor: shunt_voltage: name: 'INA3221 Channel 1 Shunt Voltage' update_interval: 15s + i2c_id: i2c_bus - 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 @@ -644,6 +694,7 @@ sensor: name: 'MPU6050 Gyro z' temperature: name: 'MPU6050 Temperature' + i2c_id: i2c_bus - platform: ms5611 temperature: name: 'Outside Temperature' @@ -651,6 +702,29 @@ sensor: 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 @@ -721,10 +795,12 @@ sensor: 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' @@ -738,6 +814,20 @@ sensor: altitude_compensation: 10m ambient_pressure_compensation: 961mBar temperature_offset: 4.2C + i2c_id: i2c_bus + - platform: scd4x + co2: + name: "SCD4X CO2" + 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' @@ -747,6 +837,7 @@ sensor: accuracy_decimals: 1 address: 0x58 update_interval: 5s + i2c_id: i2c_bus - platform: sps30 pm_1_0: name: 'Workshop PM <1µm Weight concentration' @@ -777,6 +868,7 @@ sensor: id: 'workshop_PMC_10_0' address: 0x69 update_interval: 10s + i2c_id: i2c_bus - platform: sht4x temperature: name: 'SHT4X Temperature' @@ -784,6 +876,7 @@ sensor: name: 'SHT4X Humidity' address: 0x44 update_interval: 15s + i2c_id: i2c_bus - platform: shtcx temperature: name: 'Living Room Temperature 10' @@ -791,6 +884,7 @@ sensor: name: 'Living Room Humidity 10' address: 0x70 update_interval: 15s + i2c_id: i2c_bus - platform: template name: 'Template Sensor' state_class: measurement @@ -816,6 +910,27 @@ sensor: 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: ultrasonic trigger_pin: GPIO25 echo_pin: @@ -855,6 +970,7 @@ sensor: name: CCS811 TVOC update_interval: 30s baseline: 0x4242 + i2c_id: i2c_bus - platform: tx20 wind_speed: name: 'Windspeed' @@ -880,6 +996,7 @@ sensor: - platform: tmp117 name: 'TMP117 Temperature' update_interval: 5s + i2c_id: i2c_bus - platform: hm3301 pm_1_0: name: 'PM1.0' @@ -890,6 +1007,7 @@ sensor: aqi: name: 'AQI' calculation_type: 'CAQI' + i2c_id: i2c_bus - platform: teleinfo tag_name: "HCHC" name: "hchc" @@ -899,10 +1017,18 @@ sensor: - 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: @@ -1036,10 +1162,6 @@ binary_sensor: pin: GPIO27 threshold: 1000 id: btn_left - - platform: nextion - page_id: 0 - component_id: 2 - name: 'Nextion Component 2 Touch' - platform: template name: 'Garage Door Open' id: garage_door @@ -1149,14 +1271,18 @@ binary_sensor: 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 @@ -1296,6 +1422,7 @@ output: id: dac_output - platform: mcp4725 id: mcp4725_dac_output + i2c_id: i2c_bus e131: @@ -1364,6 +1491,16 @@ light: 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 @@ -1371,6 +1508,12 @@ light: 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 @@ -1489,6 +1632,7 @@ light: - id: addr2 from: 20 to: 25 + - single_light_id: ${roomname}_lights remote_transmitter: - pin: 32 @@ -1502,6 +1646,22 @@ climate: 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 @@ -1525,8 +1685,92 @@ climate: 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 + id: midea_unit + uart_id: uart0 + 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 + +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_TOGGLE_LIGHT + turn_on_action: + midea_ac.display_toggle: + - platform: template + name: MIDEA_AC_SWING_STEP + turn_on_action: + midea_ac.swing_step: + - 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: @@ -1580,6 +1824,12 @@ switch: 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: @@ -1683,6 +1933,8 @@ switch: state: yes - platform: restart name: 'Living Room Restart' + - platform: safe_mode + name: 'Living Room Restart (Safe Mode)' - platform: shutdown name: 'Living Room Shutdown' - platform: output @@ -1752,6 +2004,7 @@ switch: inverted: False - platform: template id: ble1_status + optimistic: true fan: - platform: binary @@ -1760,6 +2013,8 @@ fan: 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' @@ -1767,8 +2022,13 @@ fan: 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!" interval: - interval: 10s @@ -1822,6 +2082,7 @@ display: address: 0x3F lambda: |- it.print("Hello World!"); + i2c_id: i2c_bus - platform: max7219 cs_pin: GPIO23 num_chips: 1 @@ -1843,11 +2104,6 @@ display: intensity: 3 lambda: |- it.print("1234"); - - platform: nextion - uart_id: uart0 - lambda: |- - it.set_component_value("gauge", 50); - it.set_component_text("textview", "Hello World!"); - platform: pcd8544 cs_pin: GPIO23 dc_pin: GPIO23 @@ -1860,7 +2116,7 @@ display: reset_pin: GPIO23 address: 0x3C id: display1 - brightness: 60% + contrast: 60% pages: - id: page1 lambda: |- @@ -1874,6 +2130,7 @@ display: then: lambda: |- ESP_LOGD("display", "1 -> 2"); + i2c_id: i2c_bus - platform: ssd1306_spi model: 'SSD1306 128x64' cs_pin: GPIO23 @@ -1908,6 +2165,7 @@ display: - id: page13272 lambda: |- // Nothing + i2c_id: i2c_bus - platform: ssd1327_spi model: 'SSD1327 128x128' cs_pin: GPIO23 @@ -1935,6 +2193,14 @@ display: 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 @@ -1978,6 +2244,7 @@ pn532_spi: payload: !lambda 'return x;' pn532_i2c: + i2c_id: i2c_bus rdm6300: uart_id: uart0 @@ -1994,11 +2261,13 @@ rc522_i2c: 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 gps: uart_id: uart0 @@ -2025,6 +2294,7 @@ time: on_time: seconds: 0 then: ds1307.read_time + i2c_id: i2c_bus cover: - platform: template @@ -2042,37 +2312,58 @@ cover: id: template_cover state: CLOSED assumed_state: no + has_position: yes + 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 debug: tca9548a: - address: 0x70 id: multiplex0 - scan: True + channels: + - bus_id: multiplex0_chan0 + channel: 0 + i2c_id: i2c_bus - address: 0x71 id: multiplex1 - scan: True - multiplexer: - id: multiplex0 - channel: 0 + i2c_id: multiplex0_chan0 pcf8574: - id: 'pcf8574_hub' address: 0x21 pcf8575: False + 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 @@ -2125,6 +2416,8 @@ text_sensor: name: Template Text Sensor id: ${textname}_text - platform: wifi_info + scan_results: + name: 'Scan Results' ip_address: name: 'IP Address' ssid: diff --git a/tests/test2.yaml b/tests/test2.yaml index faa76300cc..7e71d1ab4e 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -14,13 +14,15 @@ ethernet: clk_mode: GPIO0_IN phy_addr: 0 power_pin: GPIO25 - enable_mdns: false 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: @@ -59,6 +61,12 @@ 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 @@ -89,6 +97,8 @@ sensor: name: 'b-parasite Soil Moisture' battery_voltage: name: 'b-parasite Battery Voltage' + illuminance: + name: 'b-parasite Illuminance' - platform: senseair id: senseair0 co2: @@ -218,6 +228,16 @@ sensor: 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: @@ -226,6 +246,20 @@ sensor: name: 'Inkbird IBS-TH1 Humidity' battery_level: name: 'Inkbird IBS-TH1 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: sgp40 name: 'Workshop VOC' update_interval: 5s @@ -236,6 +270,35 @@ sensor: 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" + time: - platform: homeassistant on_time: @@ -266,6 +329,11 @@ binary_sensor: - 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 @@ -293,6 +361,16 @@ binary_sensor: 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' esp32_ble_tracker: on_ble_advertise: @@ -314,6 +392,15 @@ esp32_ble_tracker: - 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 + + +airthings_ble: + #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' @@ -411,3 +498,4 @@ interval: - logger.log: 'Interval Run' display: + diff --git a/tests/test3.yaml b/tests/test3.yaml index af5398b604..73e314c94c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -5,10 +5,13 @@ esphome: board: d1_mini build_path: build/test3 on_boot: - - wait_until: - - api.connected - - wifi.connected - - time.has_time + - if: + condition: + - api.connected + - wifi.connected + - time.has_time + then: + - logger.log: "Have time" includes: - custom.h @@ -22,6 +25,8 @@ api: port: 8000 password: 'pwd' reboot_timeout: 0min + encryption: + key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=' services: - service: hello_world variables: @@ -257,9 +262,7 @@ ota: logger: hardware_uart: UART1 level: DEBUG - esp8266_store_log_strings_in_flash: false - -web_server: + esp8266_store_log_strings_in_flash: true deep_sleep: run_duration: 20s @@ -269,7 +272,39 @@ 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: apds9960 type: proximity name: APDS9960 Proximity @@ -392,14 +427,19 @@ sensor: 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: pzem004t uart_id: uart3 voltage: @@ -449,6 +489,66 @@ sensor: 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' + - platform: pmsx003 + uart_id: uart2 + type: PMS5003T + pm_2_5: + name: 'PM 2.5 Concentration' + temperature: + name: 'PMS Temperature' + humidity: + name: 'PMS Humidity' + - platform: pmsx003 + uart_id: uart2 + 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: cse7766 uart_id: uart3 voltage: @@ -533,7 +633,19 @@ sensor: 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 time: - platform: homeassistant @@ -546,6 +658,11 @@ mpr121: address: 0x5A binary_sensor: + - platform: daly_bms + charging_mos_enabled: + name: "Charging MOS" + discharging_mos_enabled: + name: "Discharging MOS" - platform: apds9960 direction: up name: APDS9960 Up @@ -605,6 +722,19 @@ binary_sensor: binary_sensors: - id: custom_binary_sensor name: Custom Binary Sensor + - 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: template + id: 'cover_toggle' + on_press: + then: + - cover.toggle: time_based_cover globals: - id: my_global_string @@ -619,6 +749,9 @@ status_led: pin: GPIO2 text_sensor: + - platform: daly_bms + status: + name: "BMS Status" - platform: version name: 'ESPHome Version' icon: mdi:icon @@ -653,22 +786,21 @@ text_sensor: text_sensors: - id: custom_text_sensor name: Custom Text Sensor + - 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!");' - - id: climate_custom - then: - - climate.control: - id: midea_ac_unit - custom_preset: FREEZE_PROTECTION - custom_fan_mode: SILENT - - id: climate_preset - then: - - climate.control: - id: midea_ac_unit - preset: SLEEP sm2135: data_pin: GPIO12 @@ -704,6 +836,10 @@ switch: switches: - id: custom_switch name: Custom Switch + - platform: nextion + id: r0 + name: 'R0 Switch' + component_name: page0.r0 custom_component: lambda: |- @@ -772,8 +908,12 @@ climate: - 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: @@ -816,7 +956,28 @@ climate: - switch.turn_on: gpio_switch1 swing_both_action: - switch.turn_on: gpio_switch2 - hysteresis: 0.2 + 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 away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C @@ -830,32 +991,6 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 - - platform: midea_ac - id: midea_ac_unit - visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: "Electrolux EACS" - beeper: true - custom_fan_modes: - - SILENT - - TURBO - preset_eco: true - preset_sleep: true - preset_boost: true - custom_presets: - - FREEZE_PROTECTION - outdoor_temperature: - name: "Temp" - power_usage: - name: "Power" - humidity_setpoint: - name: "Hum" - -midea_dongle: - uart_id: uart1 - strength_icon: true cover: - platform: endstop @@ -890,6 +1025,7 @@ cover: max_duration: 10min - platform: time_based name: Time Based Cover + id: time_based_cover stop_action: - switch.turn_on: gpio_switch1 open_action: @@ -898,6 +1034,29 @@ cover: close_action: - switch.turn_on: gpio_switch2 close_duration: 4.5min + - platform: current_based + name: "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;' @@ -934,10 +1093,6 @@ output: return {s}; outputs: - id: custom_float - - platform: ac_dimmer - id: dimmer1 - gate_pin: GPIO5 - zero_cross_pin: GPIO12 - platform: slow_pwm pin: GPIO5 id: my_slow_pwm @@ -1086,6 +1241,16 @@ display: id: my_matrix lambda: |- it.printdigit("hello"); + - platform: nextion + uart_id: uart1 + 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");' http_request: useragent: esphome/device @@ -1097,27 +1262,35 @@ fingerprint_grow: new_password: 0xA65B9840 on_finger_scan_matched: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_finger_scan_matched + 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.${devicename}_fingerprint_grow_finger_scan_unmatched + event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched on_enrollment_scan: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_enrollment_scan + 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.${devicename}_fingerprint_grow_node_enrollment_done + event: esphome.${device_name}_fingerprint_grow_node_enrollment_done data: finger_id: !lambda 'return finger_id;' on_enrollment_failed: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_enrollment_failed + event: esphome.${device_name}_fingerprint_grow_enrollment_failed data: finger_id: !lambda 'return finger_id;' uart_id: uart6 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + uart_id: uart6 + +daly_bms: + update_interval: 20s + uart_id: uart1 diff --git a/tests/test4.yaml b/tests/test4.yaml index 7868fd4968..4f2025ad74 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -56,6 +56,9 @@ time: tuya: time_id: sntp_time +pipsolar: + id: inverter0 + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -63,6 +66,149 @@ sensor: - 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 sensor.apds9960 requires component apds9960 # @@ -86,6 +232,59 @@ 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 {};' @@ -122,6 +321,20 @@ 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 light: - platform: fastled_clockless @@ -145,6 +358,11 @@ light: warm_white_color_temperature: 500 mireds gamma_correct: 1 +cover: + - platform: tuya + id: tuya_cover + position_datapoint: 2 + display: - platform: addressable_light id: led_matrix_32x8_display @@ -166,7 +384,7 @@ display: 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 @@ -194,7 +412,48 @@ display: 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 + + + +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 + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out esp32_camera: name: ESP-32 Camera data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] diff --git a/tests/test5.yaml b/tests/test5.yaml index ba047721e2..72df3ed212 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -1,12 +1,17 @@ esphome: name: test5 - platform: ESP32 - board: nodemcu-32s 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' @@ -18,16 +23,53 @@ ota: logger: +uart: + - id: uart1 + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + - id: uart2 + tx_pin: 17 + rx_pin: 16 + baud_rate: 19200 + +i2c: + + +modbus: + uart_id: uart1 + flow_control_pin: 5 + id: mod_bus1 + +modbus_controller: + - id: modbus_controller_test + address: 0x2 + modbus_id: mod_bus1 + + binary_sensor: - platform: gpio pin: GPIO0 id: io0_button + icon: mdi:gesture-tap-button + +tlc5947: + data_pin: GPIO12 + clock_pin: GPIO14 + lat_pin: GPIO15 output: - platform: gpio pin: GPIO2 id: built_in_led + - platform: tlc5947 + id: output_red + channel: 0 + max_power: 0.8 + +demo: + esp32_ble: esp32_ble_server: @@ -38,3 +80,96 @@ esp32_improv: authorizer: io0_button authorized_duration: 1min status_indicator: built_in_led + +number: + - platform: template + name: My template number + id: template_number_id + optimistic: true + on_value: + - logger.log: + format: "Number changed to %f" + args: ["x"] + set_action: + - logger.log: + format: "Template Number set to %f" + args: ["x"] + max_value: 100 + min_value: 0 + step: 5 + +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" + args: ["x.c_str()"] + set_action: + - logger.log: + format: "Template Select set to %s" + args: ["x.c_str()"] + - select.set: + id: template_select_id + option: two + options: + - one + - two + - three + +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 + + - id: battery_voltage + name: "Battery voltage2" + platform: modbus_controller + modbus_controller_id: modbus_controller_test + address: 0x331A + register_type: read + value_type: U_WORD + + - platform: t6615 + uart_id: uart2 + co2: + name: CO2 Sensor diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py index 4bc0482f5f..30768f9d56 100644 --- a/tests/unit_tests/strategies.py +++ b/tests/unit_tests/strategies.py @@ -4,7 +4,7 @@ import hypothesis.strategies._internal.core as st from hypothesis.strategies._internal.strategies import SearchStrategy -@st.defines_strategy_with_reusable_values +@st.defines_strategy(force_reusable_values=True) def mac_addr_strings(): # type: () -> SearchStrategy[Text] """A strategy for MAC address strings. diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 9f402465fa..32d82b3062 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -59,7 +59,7 @@ from esphome import codegen as cg "NAN", "esphome_ns", "App", - "Nameable", + "EntityBase", "Component", "ComponentPtr", # from cpp_types diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 16cfb16e94..e34c7064fa 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -40,6 +40,28 @@ def test_valid_name__invalid(value): config_validation.valid_name(value) +@pytest.mark.parametrize("value", ("foo", "bar123", "foo-bar")) +def test_hostname__valid(value): + actual = config_validation.hostname(value) + + assert actual == value + + +@pytest.mark.parametrize("value", ("foo bar", "foobar ", "foo#bar")) +def test_hostname__invalid(value): + with pytest.raises(Invalid): + config_validation.hostname(value) + + +def test_hostname__warning(caplog): + actual = config_validation.hostname("foo_bar") + assert actual == "foo_bar" + assert ( + "Using the '_' (underscore) character in the hostname is discouraged" + in caplog.text + ) + + @given(one_of(integers(), text())) def test_string__valid(value): actual = config_validation.string(value) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 4e60880033..9a15bf0b9c 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -444,16 +444,24 @@ class TestDefine: class TestLibrary: @pytest.mark.parametrize( - "name, value, prop, expected", + "name, version, repository, prop, expected", ( - ("mylib", None, "as_lib_dep", "mylib"), - ("mylib", None, "as_tuple", ("mylib", None)), - ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), - ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), + ("mylib", None, None, "as_lib_dep", "mylib"), + ("mylib", None, None, "as_tuple", ("mylib", None, None)), + ("mylib", "1.2.3", None, "as_lib_dep", "mylib@1.2.3"), + ("mylib", "1.2.3", None, "as_tuple", ("mylib", "1.2.3", None)), + ("mylib", None, "file:///test", "as_lib_dep", "mylib=file:///test"), + ( + "mylib", + None, + "file:///test", + "as_tuple", + ("mylib", None, "file:///test"), + ), ), ) - def test_properties(self, name, value, prop, expected): - target = core.Library(name, value) + def test_properties(self, name, version, repository, prop, expected): + target = core.Library(name, version, repository) actual = getattr(target, prop) @@ -465,6 +473,11 @@ class TestLibrary: ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), ("__eq__", core.Library(name="libbar", version="1.2.3"), False), + ( + "__eq__", + core.Library(name="libbar", version=None, repository="file:///test"), + False, + ), ("__eq__", 1000, NotImplemented), ("__eq__", "1000", NotImplemented), ("__eq__", True, NotImplemented), @@ -518,13 +531,13 @@ class TestEsphomeCore: assert target.address == "4.3.2.1" def test_is_esp32(self, target): - target.esp_platform = "ESP32" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp32"} assert target.is_esp32 is True assert target.is_esp8266 is False def test_is_esp8266(self, target): - target.esp_platform = "ESP8266" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp8266"} assert target.is_esp32 is False assert target.is_esp8266 is True diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 3e317589a9..ad234250ce 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -3,7 +3,6 @@ from mock import Mock from esphome import cpp_helpers as ch from esphome import const -from esphome.cpp_generator import MockObj @pytest.mark.asyncio @@ -13,15 +12,6 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): assert actual is None -@pytest.mark.asyncio -async def test_gpio_pin_expression__new_pin(monkeypatch): - actual = await ch.gpio_pin_expression( - {const.CONF_NUMBER: 42, const.CONF_MODE: "input", const.CONF_INVERTED: False} - ) - - assert isinstance(actual, MockObj) - - @pytest.mark.asyncio async def test_register_component(monkeypatch): var = Mock(base="foo.bar") @@ -38,7 +28,7 @@ async def test_register_component(monkeypatch): actual = await ch.register_component(var, {}) assert actual is var - add_mock.assert_called_once() + assert add_mock.call_count == 2 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] @@ -77,6 +67,6 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() - assert add_mock.call_count == 3 + assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py deleted file mode 100644 index 6bc6f4d766..0000000000 --- a/tests/unit_tests/test_pins.py +++ /dev/null @@ -1,346 +0,0 @@ -""" -Please Note: - -These tests cover the process of identifying information about pins, they do not -check if the definition of MCUs and pins is correct. - -""" -import logging - -import pytest - -from esphome.config_validation import Invalid -from esphome.core import EsphomeCore -from esphome import pins - - -MOCK_ESP8266_BOARD_ID = "_mock_esp8266" -MOCK_ESP8266_PINS = {"X0": 16, "X1": 5, "X2": 4, "LED": 2} -MOCK_ESP8266_BOARD_ALIAS_ID = "_mock_esp8266_alias" -MOCK_ESP8266_FLASH_SIZE = pins.FLASH_SIZE_2_MB - -MOCK_ESP32_BOARD_ID = "_mock_esp32" -MOCK_ESP32_PINS = {"Y0": 12, "Y1": 8, "Y2": 3, "LED": 9, "A0": 8} -MOCK_ESP32_BOARD_ALIAS_ID = "_mock_esp32_alias" - -UNKNOWN_PLATFORM = "STM32" - - -@pytest.fixture -def mock_mcu(monkeypatch): - """ - Add a mock MCU into the lists as a stable fixture - """ - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID - yield - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] - - -@pytest.fixture -def core(monkeypatch, mock_mcu): - core = EsphomeCore() - monkeypatch.setattr(pins, "CORE", core) - return core - - -@pytest.fixture -def core_esp8266(core): - core.esp_platform = "ESP8266" - core.board = MOCK_ESP8266_BOARD_ID - return core - - -@pytest.fixture -def core_esp32(core): - core.esp_platform = "ESP32" - core.board = MOCK_ESP32_BOARD_ID - return core - - -class Test_lookup_pin: - @pytest.mark.parametrize( - "value, expected", - ( - ("X1", 5), - ("MOSI", 13), - ), - ) - def test_valid_esp8266_pin(self, core_esp8266, value, expected): - actual = pins._lookup_pin(value) - - assert actual == expected - - def test_valid_esp8266_pin_alias(self, core_esp8266): - core_esp8266.board = MOCK_ESP8266_BOARD_ALIAS_ID - - actual = pins._lookup_pin("X2") - - assert actual == 4 - - @pytest.mark.parametrize( - "value, expected", - ( - ("Y1", 8), - ("A0", 8), - ("MOSI", 23), - ), - ) - def test_valid_esp32_pin(self, core_esp32, value, expected): - actual = pins._lookup_pin(value) - - assert actual == expected - - def test_valid_32_pin_alias(self, core_esp32): - core_esp32.board = MOCK_ESP32_BOARD_ALIAS_ID - - actual = pins._lookup_pin("Y2") - - assert actual == 3 - - def test_invalid_pin(self, core_esp8266): - with pytest.raises( - Invalid, match="Cannot resolve pin name 'X42' for board _mock_esp8266." - ): - pins._lookup_pin("X42") - - def test_unsupported_platform(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins._lookup_pin("TX") - - -class Test_translate_pin: - @pytest.mark.parametrize( - "value, expected", - ( - (2, 2), - ("3", 3), - ("GPIO4", 4), - ("TX", 1), - ("Y0", 12), - ), - ) - def test_valid_values(self, core_esp32, value, expected): - actual = pins._translate_pin(value) - - assert actual == expected - - @pytest.mark.parametrize("value", ({}, None)) - def test_invalid_values(self, core_esp32, value): - with pytest.raises(Invalid, match="This variable only supports"): - pins._translate_pin(value) - - -class Test_validate_gpio_pin: - def test_esp32_valid(self, core_esp32): - actual = pins.validate_gpio_pin("GPIO22") - - assert actual == 22 - - @pytest.mark.parametrize( - "value, match", - ( - (-1, "ESP32: Invalid pin number: -1"), - (40, "ESP32: Invalid pin number: 40"), - (6, "This pin cannot be used on ESP32s and"), - (7, "This pin cannot be used on ESP32s and"), - (8, "This pin cannot be used on ESP32s and"), - (11, "This pin cannot be used on ESP32s and"), - (20, "The pin GPIO20 is not usable on ESP32s"), - (24, "The pin GPIO24 is not usable on ESP32s"), - (28, "The pin GPIO28 is not usable on ESP32s"), - (29, "The pin GPIO29 is not usable on ESP32s"), - (30, "The pin GPIO30 is not usable on ESP32s"), - (31, "The pin GPIO31 is not usable on ESP32s"), - ), - ) - def test_esp32_invalid_pin(self, core_esp32, value, match): - with pytest.raises(Invalid, match=match): - pins.validate_gpio_pin(value) - - @pytest.mark.parametrize("value", (9, 10)) - def test_esp32_warning(self, core_esp32, caplog, value): - caplog.at_level(logging.WARNING) - pins.validate_gpio_pin(value) - - assert len(caplog.messages) == 1 - assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.") - - def test_esp8266_valid(self, core_esp8266): - actual = pins.validate_gpio_pin("GPIO12") - - assert actual == 12 - - @pytest.mark.parametrize( - "value, match", - ( - (-1, "ESP8266: Invalid pin number: -1"), - (18, "ESP8266: Invalid pin number: 18"), - (6, "This pin cannot be used on ESP8266s and"), - (7, "This pin cannot be used on ESP8266s and"), - (8, "This pin cannot be used on ESP8266s and"), - (11, "This pin cannot be used on ESP8266s and"), - ), - ) - def test_esp8266_invalid_pin(self, core_esp8266, value, match): - with pytest.raises(Invalid, match=match): - pins.validate_gpio_pin(value) - - @pytest.mark.parametrize("value", (9, 10)) - def test_esp8266_warning(self, core_esp8266, caplog, value): - caplog.at_level(logging.WARNING) - pins.validate_gpio_pin(value) - - assert len(caplog.messages) == 1 - assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.") - - def test_unknown_device(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins.validate_gpio_pin("0") - - -class Test_input_pin: - @pytest.mark.parametrize("value, expected", (("X0", 16),)) - def test_valid_esp8266_values(self, core_esp8266, value, expected): - actual = pins.input_pin(value) - - assert actual == expected - - @pytest.mark.parametrize( - "value, expected", - ( - ("Y0", 12), - (17, 17), - ), - ) - def test_valid_esp32_values(self, core_esp32, value, expected): - actual = pins.input_pin(value) - - assert actual == expected - - @pytest.mark.parametrize("value", (17,)) - def test_invalid_esp8266_values(self, core_esp8266, value): - with pytest.raises(Invalid): - pins.input_pin(value) - - def test_unknown_platform(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins.input_pin(2) - - -class Test_input_pullup_pin: - @pytest.mark.parametrize("value, expected", (("X0", 16),)) - def test_valid_esp8266_values(self, core_esp8266, value, expected): - actual = pins.input_pullup_pin(value) - - assert actual == expected - - @pytest.mark.parametrize( - "value, expected", - ( - ("Y0", 12), - (17, 17), - ), - ) - def test_valid_esp32_values(self, core_esp32, value, expected): - actual = pins.input_pullup_pin(value) - - assert actual == expected - - @pytest.mark.parametrize("value", (0,)) - def test_invalid_esp8266_values(self, core_esp8266, value): - with pytest.raises(Invalid): - pins.input_pullup_pin(value) - - def test_unknown_platform(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins.input_pullup_pin(2) - - -class Test_output_pin: - @pytest.mark.parametrize("value, expected", (("X0", 16),)) - def test_valid_esp8266_values(self, core_esp8266, value, expected): - actual = pins.output_pin(value) - - assert actual == expected - - @pytest.mark.parametrize( - "value, expected", - ( - ("Y0", 12), - (17, 17), - ), - ) - def test_valid_esp32_values(self, core_esp32, value, expected): - actual = pins.output_pin(value) - - assert actual == expected - - @pytest.mark.parametrize("value", (17,)) - def test_invalid_esp8266_values(self, core_esp8266, value): - with pytest.raises(Invalid): - pins.output_pin(value) - - @pytest.mark.parametrize("value", range(34, 40)) - def test_invalid_esp32_values(self, core_esp32, value): - with pytest.raises(Invalid): - pins.output_pin(value) - - def test_unknown_platform(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins.output_pin(2) - - -class Test_analog_pin: - @pytest.mark.parametrize("value, expected", ((17, 17),)) - def test_valid_esp8266_values(self, core_esp8266, value, expected): - actual = pins.analog_pin(value) - - assert actual == expected - - @pytest.mark.parametrize( - "value, expected", - ( - (32, 32), - (39, 39), - ), - ) - def test_valid_esp32_values(self, core_esp32, value, expected): - actual = pins.analog_pin(value) - - assert actual == expected - - @pytest.mark.parametrize("value", ("X0",)) - def test_invalid_esp8266_values(self, core_esp8266, value): - with pytest.raises(Invalid): - pins.analog_pin(value) - - @pytest.mark.parametrize("value", ("Y0",)) - def test_invalid_esp32_values(self, core_esp32, value): - with pytest.raises(Invalid): - pins.analog_pin(value) - - def test_unknown_platform(self, core): - core.esp_platform = UNKNOWN_PLATFORM - - with pytest.raises(NotImplementedError): - pins.analog_pin(2) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 0ca7c83e1b..18e040b0a6 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -2,7 +2,7 @@ import esphome.wizard as wz import pytest -from esphome.pins import ESP8266_BOARD_PINS +from esphome.components.esp8266.boards import ESP8266_BOARD_PINS from mock import MagicMock